@nodefony/devkit 10.0.0-alpha.1 → 10.0.0-alpha.2

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 (25) hide show
  1. package/package.json +7 -7
  2. package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorate.js +0 -9
  3. package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateMetadata.js +0 -6
  4. package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateParam.js +0 -8
  5. package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js +0 -9
  6. package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateMetadata.js +0 -6
  7. package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateParam.js +0 -8
  8. package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorate.js +0 -9
  9. package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorateMetadata.js +0 -6
  10. package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorateParam.js +0 -8
  11. package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorate.js +0 -9
  12. package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorateMetadata.js +0 -6
  13. package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorateParam.js +0 -8
  14. package/dist/nodefony/command/CardCommand.js +0 -70
  15. package/dist/nodefony/controllers/OAuthMetadataController.js +0 -89
  16. package/dist/nodefony/src/mcp/guard.js +0 -51
  17. package/dist/nodefony/src/mcp/protocol.js +0 -127
  18. package/dist/nodefony/src/mcp/server.js +0 -133
  19. package/dist/nodefony/src/mcp/tools.js +0 -163
  20. package/dist/types/nodefony/command/CardCommand.d.ts +0 -33
  21. package/dist/types/nodefony/controllers/OAuthMetadataController.d.ts +0 -39
  22. package/dist/types/nodefony/src/mcp/guard.d.ts +0 -66
  23. package/dist/types/nodefony/src/mcp/protocol.d.ts +0 -139
  24. package/dist/types/nodefony/src/mcp/server.d.ts +0 -48
  25. package/dist/types/nodefony/src/mcp/tools.d.ts +0 -81
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodefony/devkit",
3
- "version": "10.0.0-alpha.1",
3
+ "version": "10.0.0-alpha.2",
4
4
  "type": "module",
5
5
  "description": "Outillage de développement d'une application Nodefony : carte de visite du projet et portes de découverte pour un agent de développement",
6
6
  "author": "Christophe CAMENSULI <ccamensuli@gmail.com>",
@@ -25,17 +25,17 @@
25
25
  "coverage": "vitest run --coverage"
26
26
  },
27
27
  "peerDependencies": {
28
- "@nodefony/framework": "*",
29
- "@nodefony/http": "*",
30
- "nodefony": "*",
28
+ "@nodefony/framework": "^10.0.0-alpha.2",
29
+ "@nodefony/http": "^10.0.0-alpha.2",
30
+ "nodefony": "^10.0.0-alpha.2",
31
31
  "zod": "^4.4.3",
32
32
  "playwright": "^1.50.0",
33
33
  "lighthouse": "^13.0.0"
34
34
  },
35
35
  "devDependencies": {
36
- "@nodefony/framework": "*",
37
- "@nodefony/http": "*",
38
- "nodefony": "*"
36
+ "@nodefony/framework": "^10.0.0-alpha.2",
37
+ "@nodefony/http": "^10.0.0-alpha.2",
38
+ "nodefony": "^10.0.0-alpha.2"
39
39
  },
40
40
  "dependencies": {
41
41
  "axe-core": "4.13.0",
@@ -1,9 +0,0 @@
1
- //#region \0@oxc-project+runtime@0.142.0/helpers/esm/decorate.js
2
- function __decorate(decorators, target, key, desc) {
3
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
- return c > 3 && r && Object.defineProperty(target, key, r), r;
7
- }
8
- //#endregion
9
- export { __decorate as default };
@@ -1,6 +0,0 @@
1
- //#region \0@oxc-project+runtime@0.142.0/helpers/esm/decorateMetadata.js
2
- function __decorateMetadata(k, v) {
3
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
4
- }
5
- //#endregion
6
- export { __decorateMetadata as default };
@@ -1,8 +0,0 @@
1
- //#region \0@oxc-project+runtime@0.142.0/helpers/esm/decorateParam.js
2
- function __decorateParam(paramIndex, decorator) {
3
- return function(target, key) {
4
- decorator(target, key, paramIndex);
5
- };
6
- }
7
- //#endregion
8
- export { __decorateParam as default };
@@ -1,9 +0,0 @@
1
- //#region \0@oxc-project+runtime@0.143.0/helpers/esm/decorate.js
2
- function __decorate(decorators, target, key, desc) {
3
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
- return c > 3 && r && Object.defineProperty(target, key, r), r;
7
- }
8
- //#endregion
9
- export { __decorate as default };
@@ -1,6 +0,0 @@
1
- //#region \0@oxc-project+runtime@0.143.0/helpers/esm/decorateMetadata.js
2
- function __decorateMetadata(k, v) {
3
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
4
- }
5
- //#endregion
6
- export { __decorateMetadata as default };
@@ -1,8 +0,0 @@
1
- //#region \0@oxc-project+runtime@0.143.0/helpers/esm/decorateParam.js
2
- function __decorateParam(paramIndex, decorator) {
3
- return function(target, key) {
4
- decorator(target, key, paramIndex);
5
- };
6
- }
7
- //#endregion
8
- export { __decorateParam as default };
@@ -1,9 +0,0 @@
1
- //#region \0@oxc-project+runtime@0.146.0/helpers/esm/decorate.js
2
- function __decorate(decorators, target, key, desc) {
3
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
- return c > 3 && r && Object.defineProperty(target, key, r), r;
7
- }
8
- //#endregion
9
- export { __decorate as default };
@@ -1,6 +0,0 @@
1
- //#region \0@oxc-project+runtime@0.146.0/helpers/esm/decorateMetadata.js
2
- function __decorateMetadata(k, v) {
3
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
4
- }
5
- //#endregion
6
- export { __decorateMetadata as default };
@@ -1,8 +0,0 @@
1
- //#region \0@oxc-project+runtime@0.146.0/helpers/esm/decorateParam.js
2
- function __decorateParam(paramIndex, decorator) {
3
- return function(target, key) {
4
- decorator(target, key, paramIndex);
5
- };
6
- }
7
- //#endregion
8
- export { __decorateParam as default };
@@ -1,9 +0,0 @@
1
- //#region \0@oxc-project+runtime@0.147.0/helpers/esm/decorate.js
2
- function __decorate(decorators, target, key, desc) {
3
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
- return c > 3 && r && Object.defineProperty(target, key, r), r;
7
- }
8
- //#endregion
9
- export { __decorate as default };
@@ -1,6 +0,0 @@
1
- //#region \0@oxc-project+runtime@0.147.0/helpers/esm/decorateMetadata.js
2
- function __decorateMetadata(k, v) {
3
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
4
- }
5
- //#endregion
6
- export { __decorateMetadata as default };
@@ -1,8 +0,0 @@
1
- //#region \0@oxc-project+runtime@0.147.0/helpers/esm/decorateParam.js
2
- function __decorateParam(paramIndex, decorator) {
3
- return function(target, key) {
4
- decorator(target, key, paramIndex);
5
- };
6
- }
7
- //#endregion
8
- export { __decorateParam as default };
@@ -1,70 +0,0 @@
1
- import { Command } from "nodefony";
2
- //#region nodefony/command/CardCommand.ts
3
- /**
4
- * `onReady` : les services sont construits, AUCUN serveur n'écoute. La carte se
5
- * lit dans l'état du kernel — ouvrir un port pour la rendre serait payer un
6
- * démarrage complet pour une réponse qui n'en dépend pas.
7
- */
8
- const options = {
9
- showBanner: false,
10
- kernelEvent: "onReady"
11
- };
12
- /**
13
- * `nodefony devkit:card [-j]` — qui répond, et où aller ensuite.
14
- *
15
- * ## Pourquoi une commande, alors que la route existe
16
- *
17
- * La route HTTP vit sous `/nodefony`, que le pare-feu d'une application réelle
18
- * couvre : un agent qui code ne s'authentifie pas, et n'a pas de navigateur. La
19
- * porte qu'il a déjà, c'est le terminal. Même source (le service), deux rendus —
20
- * ajouter une porte n'ajoute jamais une vérité.
21
- *
22
- * ⚠️ Le module est `policy: "dev"` : hors développement il n'est pas chargé, donc
23
- * cette commande **n'existe pas**. C'est voulu — et c'est pour ça qu'elle
24
- * s'invoque `NODE_ENV=development npx nodefony devkit:card` depuis un terminal
25
- * qui n'aurait pas posé la variable.
26
- */
27
- var CardCommand = class CardCommand extends Command {
28
- constructor(cli) {
29
- super("devkit:card", "Imprime la carte de visite de l application", cli, options);
30
- this.addOption("-j, --json", "sortie JSON brute (scriptable, `| jq`)");
31
- }
32
- async generate(opts) {
33
- const svc = this.kernel?.container?.get("devkit");
34
- if (!svc) {
35
- this.log("service « devkit » non enregistré — module non chargé (policy dev) ?", "ERROR");
36
- return this;
37
- }
38
- const card = svc.getCard();
39
- if (opts.json) {
40
- process.stdout.write(`${JSON.stringify(card, null, 2)}\n`);
41
- return this;
42
- }
43
- process.stdout.write(CardCommand.format(card));
44
- return this;
45
- }
46
- /**
47
- * Rend la carte pour un HUMAIN (ou un agent qui lit un terminal).
48
- *
49
- * Statique et PURE : elle ne touche ni au kernel ni au service, donc elle
50
- * s'éprouve seule. Sortie sur `stdout` plutôt que par le journal — une carte
51
- * de visite n'est pas un événement de log, et le préfixe horodaté rendrait le
52
- * copier-coller inutilisable.
53
- */
54
- static format(card) {
55
- return [
56
- `${card.app.name} ${card.app.version} — ${card.app.environment} (nodefony ${card.nodefony.version})`,
57
- "",
58
- `Modules chargés (${card.modules.length}) : ${card.modules.join(", ")}`,
59
- "",
60
- "Où aller :",
61
- ...card.portes.map((p) => ` ${p.ou}\n ${p.titre} — ${p.pourquoi}`),
62
- "",
63
- "Quoi lancer :",
64
- ...card.verbes.map((v) => ` ${v.commande}\n ${v.pourquoi}`),
65
- ""
66
- ].join("\n");
67
- }
68
- };
69
- //#endregion
70
- export { CardCommand as default };
@@ -1,89 +0,0 @@
1
- import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateMetadata.js";
2
- import __decorate from "../../_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js";
3
- import { MCP_ENDPOINT_PATH, buildProtectedResourceMetadata, protectedResourceMetadataPath } from "nodefony";
4
- import { Controller, controller, route } from "@nodefony/framework";
5
- //#region nodefony/controllers/OAuthMetadataController.ts
6
- /**
7
- * Chemin bien connu où se publient les métadonnées de la porte MCP.
8
- *
9
- * **Dérivé, jamais recopié** : il se compose du chemin de l'endpoint par la
10
- * règle d'insertion de la RFC 9728 §3.1. Le littéral qu'on serait tenté
11
- * d'écrire ici deviendrait faux le jour où la porte déménage — et l'erreur
12
- * serait parfaitement silencieuse : un client sonde ce chemin, reçoit `404`,
13
- * et conclut que l'application n'a pas d'autorisation.
14
- */
15
- const METADATA_PATH = protectedResourceMetadataPath(MCP_ENDPOINT_PATH);
16
- /**
17
- * Métadonnées OAuth 2.1 de la porte MCP — **le seul moyen normalisé** pour
18
- * qu'un agent apprenne où obtenir un jeton.
19
- *
20
- * ## Pourquoi un controller séparé
21
- *
22
- * Trois raisons, et aucune n'est esthétique. Le chemin vit **hors** du préfixe
23
- * `/nodefony` (la RFC impose `/.well-known/…` juste après l'hôte). Le document
24
- * est **public par conception** — un client doit pouvoir le lire avant d'avoir
25
- * le moindre jeton, donc il ne porte ni garde d'origine ni exigence de
26
- * localité, contrairement à la porte elle-même. Et il répond en `GET`, quand la
27
- * porte n'accepte que `POST`.
28
- *
29
- * ## Ce que publier coûte, et ce que ne rien publier coûte
30
- *
31
- * Le document ne révèle rien de sensible : l'émetteur des jetons, les scopes
32
- * compris, un nom. En revanche, ne PAS le publier a un coût mesuré — un client
33
- * qui ne le trouve pas suppose que le serveur d'autorisation est le serveur
34
- * lui-même, et part sonder des chemins qui n'existent pas. C'est exactement ce
35
- * qui a produit le bruit OAuth observé sur cette porte.
36
- *
37
- * 🔴 **Rôle éteint = `404`, jamais un document vide.** Un document sans serveur
38
- * d'autorisation apprendrait au client qu'un jeton est nécessaire sans jamais
39
- * lui dire où le demander : la spécification MCP l'interdit d'ailleurs
40
- * explicitement (« MUST include […] at least one authorization server »).
41
- */
42
- let OAuthMetadataController = class OAuthMetadataController extends Controller {
43
- constructor(context) {
44
- super("devkit-oauth-metadata", context);
45
- }
46
- /** Résout le service du module depuis le conteneur partagé. */
47
- #service() {
48
- const svc = this.get("devkit");
49
- if (!svc) throw new Error("DevkitService non enregistré");
50
- return svc;
51
- }
52
- /**
53
- * `GET /.well-known/oauth-protected-resource/nodefony/mcp` — RFC 9728 §3.
54
- *
55
- * @returns le document JSON, ou `404` si cette porte n'est pas protégée
56
- */
57
- async metadata() {
58
- const settings = this.#service().mcpSettings();
59
- const authz = settings.authorization;
60
- if (!settings.enabled || authz.authorizationServers.length === 0) return this.renderJson({ error: "not_found" }, 404);
61
- let document;
62
- try {
63
- document = buildProtectedResourceMetadata({
64
- resource: authz.resource,
65
- authorizationServers: authz.authorizationServers,
66
- scopesSupported: authz.scopesSupported,
67
- resourceName: authz.resourceName,
68
- resourceDocumentation: authz.resourceDocumentation
69
- });
70
- } catch (error) {
71
- this.log(`MCP — métadonnées impubliables : ${error.message}`, "CRITIC");
72
- return this.renderJson({ error: "server_error" }, 500);
73
- }
74
- return this.renderJson(document, 200, { "Cache-Control": "public, max-age=3600" });
75
- }
76
- };
77
- __decorate([
78
- route("devkit-mcp-protected-resource", {
79
- path: METADATA_PATH,
80
- method: "GET"
81
- }),
82
- __decorateMetadata("design:type", Function),
83
- __decorateMetadata("design:paramtypes", []),
84
- __decorateMetadata("design:returntype", Promise)
85
- ], OAuthMetadataController.prototype, "metadata", null);
86
- OAuthMetadataController = __decorate([controller(""), __decorateMetadata("design:paramtypes", [Object])], OAuthMetadataController);
87
- var OAuthMetadataController_default = OAuthMetadataController;
88
- //#endregion
89
- export { OAuthMetadataController_default as default };
@@ -1,51 +0,0 @@
1
- //#region nodefony/src/mcp/guard.ts
2
- /**
3
- * Une adresse est-elle celle de la machine locale ?
4
- *
5
- * Couvre les trois formes que Node rend selon la pile réseau : IPv4
6
- * (`127.x.x.x`), IPv6 (`::1`), et l'IPv4 encapsulée en IPv6
7
- * (`::ffff:127.0.0.1`) — la plus fréquente sur un serveur à double pile, et
8
- * celle qu'une comparaison naïve à `"127.0.0.1"` rate.
9
- *
10
- * @param address - `socket.remoteAddress`, éventuellement absent
11
- * @returns `true` si l'appel vient de cette machine
12
- */
13
- function isLocalAddress(address) {
14
- if (!address) return false;
15
- const bare = address.startsWith("::ffff:") ? address.slice(7) : address;
16
- if (bare === "::1" || bare === "localhost") return true;
17
- return /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(bare);
18
- }
19
- /**
20
- * Décide si un appel MCP peut être servi.
21
- *
22
- * ⭐ **La règle sur `Origin` est contre-intuitive, et c'est elle qui protège.**
23
- * Un client MCP légitime est un *process* (Cursor, Claude Code, un agent tiers)
24
- * : il n'envoie **aucun** `Origin`, cet en-tête étant posé par les navigateurs.
25
- * Une page web malveillante, elle, en pose **toujours** un lorsqu'elle vise
26
- * `https://localhost:5152`. Donc : *absent* → on passe ; *présent et hors
27
- * allowlist* → `403`. C'est ce qui referme le DNS rebinding, le seul vecteur
28
- * réel contre un serveur MCP local.
29
- *
30
- * L'ordre des contrôles est délibéré : la localité d'abord, parce qu'un appel
31
- * distant ne doit même pas apprendre quelles origines sont admises.
32
- *
33
- * @param input - ce que la requête présente
34
- * @param policy - les réglages du module
35
- * @returns le verdict, avec le motif quand il refuse
36
- */
37
- function checkMcpAccess(input, policy) {
38
- if (!policy.allowRemote && !isLocalAddress(input.remoteAddress)) return {
39
- allowed: false,
40
- why: `adresse non locale (${input.remoteAddress ?? "inconnue"}) — voir devkit.mcp.allowRemote`
41
- };
42
- if (input.origin !== void 0 && input.origin !== "") {
43
- if (!policy.allowedOrigins.includes(input.origin)) return {
44
- allowed: false,
45
- why: `origine « ${input.origin} » non admise — voir devkit.mcp.allowedOrigins`
46
- };
47
- }
48
- return { allowed: true };
49
- }
50
- //#endregion
51
- export { checkMcpAccess, isLocalAddress };
@@ -1,127 +0,0 @@
1
- //#region nodefony/src/mcp/protocol.ts
2
- /**
3
- * Model Context Protocol — types et constantes du transport « Streamable HTTP ».
4
- *
5
- * Révision visée : **2026-07-28**, celle qui a supprimé les sessions de niveau
6
- * protocole et le flux `GET`. C'est ce qui rend cette porte possible sans
7
- * process dédié : chaque message est un `POST` autonome, donc un redémarrage du
8
- * serveur de développement ne casse rien — le client rejoue simplement sa
9
- * requête, et la réponse vient du code qui vient d'être rechargé.
10
- *
11
- * @see https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http
12
- */
13
- /** Révision du protocole que ce serveur annonce. */
14
- const MCP_PROTOCOL_VERSION = "2026-07-28";
15
- /**
16
- * Versions que ce serveur sait servir — publiées par `server/discover` et
17
- * listées dans l'erreur `UnsupportedProtocolVersion`.
18
- *
19
- * Une seule pour l'instant, et c'est délibéré : annoncer une révision qu'on
20
- * n'a pas éprouvée reviendrait à promettre une sémantique qu'on ne tient pas.
21
- */
22
- const MCP_SUPPORTED_VERSIONS = [MCP_PROTOCOL_VERSION];
23
- /**
24
- * Clé de métadonnée par laquelle un client MODERNE déclare sa révision.
25
- *
26
- * ⭐ **C'est la différence d'ÈRE, et elle commande tout le reste.** Jusqu'à
27
- * `2025-11-25` (ère « legacy »), un client ouvrait une session par un handshake
28
- * `initialize`. Depuis `2026-07-28` (ère « modern »), il n'y a plus de session :
29
- * chaque requête porte elle-même sa version et les capacités du client, dans
30
- * `params._meta`. Un serveur qui n'écouterait que `initialize` serait un
31
- * serveur *legacy* — quelle que soit la version qu'il prétend annoncer.
32
- */
33
- const META_PROTOCOL_VERSION = "io.modelcontextprotocol/protocolVersion";
34
- /** Clé de métadonnée portant l'identité du serveur dans `server/discover`. */
35
- const META_SERVER_INFO = "io.modelcontextprotocol/serverInfo";
36
- /**
37
- * Chemin de l'endpoint MCP.
38
- *
39
- * ## Pourquoi `/nodefony/mcp`, et pas `/nodefony/devkit/api/mcp`
40
- *
41
- * Cette URL est un **contrat public** : elle est écrite dans le `.mcp.json` de
42
- * chaque utilisateur. Y faire figurer le module qui l'implémente la rendrait
43
- * caduque au premier déménagement — or ce serveur a vocation à bouger le jour
44
- * où une application voudra s'exposer en production (le devkit, lui, est
45
- * `policy: "dev"`). Le nom d'un module est un détail d'implémentation ; une URL
46
- * ne l'est pas.
47
- *
48
- * Le segment `api` est écarté pour une autre raison : il désigne le plan
49
- * d'administration JSON de Studio, avec son contrôle d'accès par rôle. Le MCP
50
- * n'est ni REST ni destiné à Studio — ranger deux protocoles sous le même
51
- * segment promettrait une parenté qui n'existe pas.
52
- *
53
- * Et `/mcp` à la racine, qui est la convention de fait ailleurs, prendrait un
54
- * chemin qui **appartient à l'application** : `/nodefony` est le préfixe
55
- * réservé du framework, donc sans collision possible.
56
- *
57
- * Constante et **non configurable** : une route décorée est statique, et un
58
- * réglage qui n'agirait pas serait pire qu'aucun réglage.
59
- */
60
- const MCP_ENDPOINT_PATH = "/nodefony/mcp";
61
- /** Codes d'erreur JSON-RPC 2.0 employés par ce serveur. */
62
- const JsonRpcError = {
63
- /** Corps illisible. */
64
- PARSE_ERROR: -32700,
65
- /** Message qui n'est pas une requête JSON-RPC valide. */
66
- INVALID_REQUEST: -32600,
67
- /** Méthode inconnue — la spec exige alors un `404` HTTP. */
68
- METHOD_NOT_FOUND: -32601,
69
- /** Paramètres absents ou mal typés. */
70
- INVALID_PARAMS: -32602,
71
- /** Échec côté serveur. */
72
- INTERNAL_ERROR: -32603
73
- };
74
- /**
75
- * Codes réservés par la spec MCP, hors plage JSON-RPC standard.
76
- *
77
- * Ils ne sont pas décoratifs : un client s'en sert pour se rattraper seul —
78
- * renégocier une version sur `-32022`, relire `tools/list` puis réessayer sur
79
- * `-32020`. Rendre un `-32600` générique à leur place le priverait de cette
80
- * reprise et transformerait un désaccord réparable en échec définitif.
81
- */
82
- const McpProtocolError = {
83
- /**
84
- * Les en-têtes HTTP contredisent le corps, ou un en-tête requis manque.
85
- * La spec impose `400` **et** ce code (`streamable-http` §Server Validation).
86
- */
87
- HEADER_MISMATCH: -32020,
88
- /**
89
- * La révision demandée n'est pas servie. La réponse **doit** lister celles
90
- * qu'on sert, sans quoi le client n'a rien pour choisir.
91
- */
92
- UNSUPPORTED_PROTOCOL_VERSION: -32022
93
- };
94
- /** Fabrique une réponse de succès. */
95
- function jsonRpcSuccess(id, result) {
96
- return {
97
- jsonrpc: "2.0",
98
- id,
99
- result
100
- };
101
- }
102
- /** Fabrique une réponse d'erreur. */
103
- function jsonRpcFailure(id, code, message, data) {
104
- return {
105
- jsonrpc: "2.0",
106
- id,
107
- error: data === void 0 ? {
108
- code,
109
- message
110
- } : {
111
- code,
112
- message,
113
- data
114
- }
115
- };
116
- }
117
- /**
118
- * Un message est-il une NOTIFICATION (pas d'`id`) plutôt qu'une requête ?
119
- *
120
- * La distinction commande le statut HTTP : une notification acceptée rend
121
- * `202` **sans corps**, une requête rend son objet JSON.
122
- */
123
- function isNotification(message) {
124
- return message.id === void 0 || message.id === null;
125
- }
126
- //#endregion
127
- export { JsonRpcError, MCP_ENDPOINT_PATH, MCP_PROTOCOL_VERSION, MCP_SUPPORTED_VERSIONS, META_PROTOCOL_VERSION, META_SERVER_INFO, McpProtocolError, isNotification, jsonRpcFailure, jsonRpcSuccess };
@@ -1,133 +0,0 @@
1
- import { JsonRpcError, MCP_PROTOCOL_VERSION, MCP_SUPPORTED_VERSIONS, META_PROTOCOL_VERSION, META_SERVER_INFO, McpProtocolError, isNotification, jsonRpcFailure, jsonRpcSuccess } from "./protocol.js";
2
- import { callMcpTool, listMcpTools } from "./tools.js";
3
- //#region nodefony/src/mcp/server.ts
4
- /**
5
- * Extrait la révision déclarée dans `params._meta`, s'il y en a une.
6
- *
7
- * Un client MODERNE la pose à chaque requête ; un client LEGACY ne pose rien
8
- * et négocie par `initialize`. L'absence n'est donc pas une faute : c'est un
9
- * indice d'ère.
10
- */
11
- function metaVersion(params) {
12
- const meta = params._meta;
13
- if (typeof meta !== "object" || meta === null) return void 0;
14
- const value = meta[META_PROTOCOL_VERSION];
15
- return typeof value === "string" ? value : void 0;
16
- }
17
- /**
18
- * Contrôle la cohérence et le support de la révision annoncée.
19
- *
20
- * Deux refus distincts, et la spec impose les deux :
21
- * - **en-tête ≠ `_meta`** → `400` + `HeaderMismatch` (`-32020`). Le motif est
22
- * une vraie faille : un répartiteur de charge peut router sur l'en-tête
23
- * pendant que le serveur exécute d'après le corps — deux sources de vérité
24
- * pour une même requête.
25
- * - **révision inconnue** → `400` + `UnsupportedProtocolVersion` (`-32022`),
26
- * **avec la liste de celles qu'on sert** : c'est elle qui permet au client
27
- * de se rattraper au lieu d'abandonner.
28
- *
29
- * @returns `null` si tout va bien, sinon la réponse de refus
30
- */
31
- function checkProtocolVersion(id, params, headers) {
32
- const fromMeta = metaVersion(params);
33
- const fromHeader = headers.protocolVersion;
34
- if (fromMeta && fromHeader && fromMeta !== fromHeader) return {
35
- status: 400,
36
- body: jsonRpcFailure(id, McpProtocolError.HEADER_MISMATCH, "MCP-Protocol-Version ne correspond pas à _meta", {
37
- header: fromHeader,
38
- meta: fromMeta
39
- })
40
- };
41
- const declared = fromMeta ?? fromHeader;
42
- if (!declared) return null;
43
- if (!MCP_SUPPORTED_VERSIONS.includes(declared)) return {
44
- status: 400,
45
- body: jsonRpcFailure(id, McpProtocolError.UNSUPPORTED_PROTOCOL_VERSION, "Unsupported protocol version", {
46
- supported: [...MCP_SUPPORTED_VERSIONS],
47
- requested: declared
48
- })
49
- };
50
- return null;
51
- }
52
- /**
53
- * Traite UN message JSON-RPC.
54
- *
55
- * @param message - corps du `POST`, déjà parsé
56
- * @param context - outils autorisés et briques qui répondent
57
- * @param headers - en-têtes du transport (`MCP-Protocol-Version`)
58
- * @returns statut HTTP et corps à écrire (corps `null` = `202` sans contenu)
59
- */
60
- async function handleMcpMessage(message, context, headers = {}) {
61
- if (message === null || typeof message !== "object" || typeof message.method !== "string") return {
62
- status: 400,
63
- body: jsonRpcFailure(null, JsonRpcError.INVALID_REQUEST, "message JSON-RPC invalide : `method` manquante")
64
- };
65
- const method = message.method;
66
- if (isNotification(message)) return {
67
- status: 202,
68
- body: null
69
- };
70
- const id = message.id;
71
- const params = typeof message.params === "object" && message.params !== null ? message.params : {};
72
- const refus = checkProtocolVersion(id, params, headers);
73
- if (refus) return refus;
74
- switch (method) {
75
- case "server/discover": return {
76
- status: 200,
77
- body: jsonRpcSuccess(id, {
78
- resultType: "complete",
79
- supportedVersions: [...MCP_SUPPORTED_VERSIONS],
80
- capabilities: { tools: {} },
81
- instructions: "Outils d'introspection d'une application Nodefony : ce qui est monté (inspect), ce qui manque (check), ce qu'une API du framework signifie (symbols), et par où commencer (card).",
82
- _meta: { [META_SERVER_INFO]: context.serverInfo }
83
- })
84
- };
85
- case "initialize": return {
86
- status: 200,
87
- body: jsonRpcSuccess(id, {
88
- protocolVersion: MCP_PROTOCOL_VERSION,
89
- capabilities: { tools: {} },
90
- serverInfo: context.serverInfo
91
- })
92
- };
93
- case "ping": return {
94
- status: 200,
95
- body: jsonRpcSuccess(id, {})
96
- };
97
- case "tools/list": return {
98
- status: 200,
99
- body: jsonRpcSuccess(id, { tools: listMcpTools(context.tools) })
100
- };
101
- case "tools/call": {
102
- const name = typeof params.name === "string" ? params.name : "";
103
- const args = typeof params.arguments === "object" && params.arguments !== null ? params.arguments : {};
104
- if (!name) return {
105
- status: 400,
106
- body: jsonRpcFailure(id, JsonRpcError.INVALID_PARAMS, "`name` est requis pour `tools/call`")
107
- };
108
- let result;
109
- try {
110
- result = await callMcpTool(name, args, context.tools, context);
111
- } catch (error) {
112
- return {
113
- status: 200,
114
- body: jsonRpcFailure(id, JsonRpcError.INTERNAL_ERROR, `l'outil « ${name} » a échoué : ${error.message}`)
115
- };
116
- }
117
- if (result === null) return {
118
- status: 200,
119
- body: jsonRpcFailure(id, JsonRpcError.INVALID_PARAMS, `outil inconnu « ${name} » — voir tools/list`)
120
- };
121
- return {
122
- status: 200,
123
- body: jsonRpcSuccess(id, result)
124
- };
125
- }
126
- default: return {
127
- status: 404,
128
- body: jsonRpcFailure(id, JsonRpcError.METHOD_NOT_FOUND, `méthode inconnue « ${method} »`)
129
- };
130
- }
131
- }
132
- //#endregion
133
- export { handleMcpMessage };
@@ -1,163 +0,0 @@
1
- import { INSPECT_SUBJECTS, collectCheckReport, countCheckFindings, lookupSymbol, readAdminSubject, readSymbolsGraph } from "nodefony";
2
- //#region nodefony/src/mcp/tools.ts
3
- /** Préfixe des noms d'outils — un agent voit à qui il parle. */
4
- const PREFIX = "nodefony_";
5
- /**
6
- * Liste des sujets, rendue lisible pour la description de l'outil.
7
- *
8
- * Dérivée de {@link INSPECT_SUBJECTS} : ajouter un sujet au cœur le publie ici
9
- * sans rien réécrire, et il ne peut pas exister de sujet annoncé qu'on ne
10
- * saurait pas lire.
11
- */
12
- function subjectLines() {
13
- return Object.entries(INSPECT_SUBJECTS).map(([key, spec]) => `- \`${key}\` : ${spec.summary}`).join("\n");
14
- }
15
- /** Catalogue complet, avant filtrage par l'allowlist de configuration. */
16
- function catalogue() {
17
- return {
18
- inspect: {
19
- name: `${PREFIX}inspect`,
20
- description: `Lit l'état RÉEL de cette application Nodefony : les routes réellement montées, les modules chargés, les services enregistrés, la configuration effective et la provenance de chaque valeur, les stores de données, les entités de l'ORM. À utiliser avant d'écrire du code qui suppose une route, un service ou une clé de configuration — la réponse vient de l'application qui tourne, pas d'une lecture des sources, donc elle ne peut pas se tromper sur ce qui est chargé.
21
-
22
- Sujets disponibles :\n${subjectLines()}`,
23
- inputSchema: {
24
- type: "object",
25
- properties: {
26
- subject: {
27
- type: "string",
28
- enum: Object.keys(INSPECT_SUBJECTS),
29
- description: "Ce qu'on veut voir"
30
- },
31
- target: {
32
- type: "string",
33
- description: "Paramètre du sujet, quand il en attend un (ex. le nom d'un module pour `module`)"
34
- }
35
- },
36
- required: ["subject"]
37
- }
38
- },
39
- card: {
40
- name: `${PREFIX}card`,
41
- description: "Carte de visite de l'application : son identité, les modules chargés, où trouver la documentation, et les commandes à lancer. À utiliser en ARRIVANT sur une application inconnue, avant toute autre exploration — elle dit en un appel ce qu'il y a et où aller ensuite.",
42
- inputSchema: {
43
- type: "object",
44
- properties: {}
45
- }
46
- },
47
- check: {
48
- name: `${PREFIX}check`,
49
- description: "Diagnostic STATIQUE de l'application : classes écrites que rien n'enregistre (entité, controller ou service jamais câblé), paquets importés sans être déclarés, variables d'environnement requises absentes, modules du manifeste non installés, ports occupés, et le bilan du dernier démarrage. À utiliser APRÈS avoir écrit ou généré du code, avant de conclure que c'est fini : ni la compilation ni les tests ne voient qu'une classe n'est branchée à rien.",
50
- inputSchema: {
51
- type: "object",
52
- properties: {}
53
- }
54
- },
55
- symbols: {
56
- name: `${PREFIX}symbols`,
57
- description: "Interroge le graphe symbolique du framework : ce qu'est un symbole (classe, interface, fonction), où il est défini, ce qu'il étend ou implémente, et la première phrase de sa documentation. À utiliser AVANT d'ouvrir un fichier pour comprendre une API du framework — la réponse est immédiate et vaut pour la version réellement installée. Sans argument, rend le résumé du graphe ; avec `module`, la surface exportée d'un paquet.",
58
- inputSchema: {
59
- type: "object",
60
- properties: {
61
- name: {
62
- type: "string",
63
- description: "Nom exact du symbole (ex. `AbstractCrudService`, `IKernel`)"
64
- },
65
- module: {
66
- type: "string",
67
- description: "Paquet dont on veut la surface exportée (ex. `@nodefony/http`)"
68
- }
69
- }
70
- }
71
- }
72
- };
73
- }
74
- /**
75
- * Outils publiés, filtrés par l'allowlist de configuration.
76
- *
77
- * Une clé inconnue dans la configuration est simplement ignorée : elle ne peut
78
- * rien ouvrir. C'est le sens d'une allowlist — ce qui n'est pas nommé ICI
79
- * n'existe pas, et la faute de frappe d'un utilisateur ne peut pas activer
80
- * autre chose que ce qu'il voulait.
81
- *
82
- * ⚠️ **`Object.hasOwn` n'est pas une précaution de style.** Sans lui,
83
- * `tools: ["toString"]` résolvait une méthode héritée d'`Object.prototype` :
84
- * la valeur n'étant pas `undefined`, elle franchissait le filtre et un outil
85
- * fantôme entrait dans le catalogue publié. Trouvé par le test, pas à la
86
- * relecture.
87
- *
88
- * @param enabled - clés d'outils autorisées (`devkit.mcp.tools`)
89
- */
90
- function listMcpTools(enabled) {
91
- const all = catalogue();
92
- return enabled.filter((key) => Object.hasOwn(all, key)).map((key) => all[key]).filter((tool) => tool !== void 0);
93
- }
94
- /** Rend un contenu textuel — les données partent en JSON indenté, lisible. */
95
- function text(value, isError = false) {
96
- const rendered = typeof value === "string" ? value : JSON.stringify(value, null, 2);
97
- return isError ? {
98
- content: [{
99
- type: "text",
100
- text: rendered
101
- }],
102
- isError: true
103
- } : { content: [{
104
- type: "text",
105
- text: rendered
106
- }] };
107
- }
108
- /**
109
- * Exécute un outil par son nom public.
110
- *
111
- * Un échec métier (sujet inconnu, module absent) rend un résultat `isError`,
112
- * **pas** une erreur JSON-RPC : le protocole réserve celle-ci aux fautes de
113
- * protocole. La distinction compte pour l'agent — une erreur de protocole
114
- * signifie « tu t'y prends mal », un `isError` signifie « ta demande est
115
- * recevable, voici pourquoi elle n'aboutit pas » ; c'est la seconde qu'il peut
116
- * corriger seul.
117
- *
118
- * @param name - nom public de l'outil (`nodefony_inspect`…)
119
- * @param args - arguments fournis par l'agent
120
- * @param enabled - allowlist de configuration
121
- * @param deps - briques qui répondent réellement
122
- * @returns le résultat de l'outil, ou `null` si le nom n'est pas exposé
123
- */
124
- async function callMcpTool(name, args, enabled, deps) {
125
- if (!listMcpTools(enabled).find((tool) => tool.name === name)) return null;
126
- if (name === `${PREFIX}card`) return text(deps.getCard());
127
- if (name === `${PREFIX}inspect`) {
128
- const subject = typeof args.subject === "string" ? args.subject : "";
129
- const target = typeof args.target === "string" ? args.target : void 0;
130
- const read = await readAdminSubject(deps.broker, subject, target);
131
- if (!read.ok) return text(read.message, true);
132
- return text(read.data);
133
- }
134
- if (name === `${PREFIX}check`) {
135
- const report = await collectCheckReport(deps.projectRoot);
136
- return text({
137
- verdict: countCheckFindings(report) === 0 ? "ok" : "manquements",
138
- total: countCheckFindings(report),
139
- ...report
140
- });
141
- }
142
- if (name === `${PREFIX}symbols`) {
143
- const graph = readSymbolsGraph(deps.projectRoot);
144
- if (graph === null) return text("aucun graphe symbolique atteignable — il est publié par le paquet `nodefony` (node_modules/nodefony/.ai/symbols.json) et régénérable dans ce dépôt par `npm run generate-symbols`", true);
145
- const wanted = typeof args.name === "string" ? args.name : null;
146
- const module = typeof args.module === "string" ? args.module : null;
147
- if (wanted !== null) {
148
- const sym = lookupSymbol(graph, wanted);
149
- return sym ? text(sym) : text(`« ${wanted} » est introuvable dans le graphe (${Object.keys(graph.symbols).length} symboles)`, true);
150
- }
151
- const entries = Object.values(graph.symbols).filter((s) => module === null || s.module === module);
152
- if (module !== null) return text(entries.sort((a, b) => a.name.localeCompare(b.name)));
153
- const parPaquet = Object.create(null);
154
- for (const sym of entries) parPaquet[sym.module] = (parPaquet[sym.module] ?? 0) + 1;
155
- return text({
156
- total: entries.length,
157
- parPaquet
158
- });
159
- }
160
- return text(`outil « ${name} » publié mais non implémenté`, true);
161
- }
162
- //#endregion
163
- export { callMcpTool, listMcpTools };
@@ -1,33 +0,0 @@
1
- import { Command, CliKernel } from "nodefony";
2
- import type { IDevkitCard } from "../interfaces/IDevkitService.js";
3
- /**
4
- * `nodefony devkit:card [-j]` — qui répond, et où aller ensuite.
5
- *
6
- * ## Pourquoi une commande, alors que la route existe
7
- *
8
- * La route HTTP vit sous `/nodefony`, que le pare-feu d'une application réelle
9
- * couvre : un agent qui code ne s'authentifie pas, et n'a pas de navigateur. La
10
- * porte qu'il a déjà, c'est le terminal. Même source (le service), deux rendus —
11
- * ajouter une porte n'ajoute jamais une vérité.
12
- *
13
- * ⚠️ Le module est `policy: "dev"` : hors développement il n'est pas chargé, donc
14
- * cette commande **n'existe pas**. C'est voulu — et c'est pour ça qu'elle
15
- * s'invoque `NODE_ENV=development npx nodefony devkit:card` depuis un terminal
16
- * qui n'aurait pas posé la variable.
17
- */
18
- declare class CardCommand extends Command {
19
- constructor(cli: CliKernel);
20
- generate(opts: {
21
- json?: boolean;
22
- }): Promise<this>;
23
- /**
24
- * Rend la carte pour un HUMAIN (ou un agent qui lit un terminal).
25
- *
26
- * Statique et PURE : elle ne touche ni au kernel ni au service, donc elle
27
- * s'éprouve seule. Sortie sur `stdout` plutôt que par le journal — une carte
28
- * de visite n'est pas un événement de log, et le préfixe horodaté rendrait le
29
- * copier-coller inutilisable.
30
- */
31
- static format(card: IDevkitCard): string;
32
- }
33
- export default CardCommand;
@@ -1,39 +0,0 @@
1
- import { Controller } from "@nodefony/framework";
2
- import type { ContextType } from "@nodefony/http";
3
- /**
4
- * Métadonnées OAuth 2.1 de la porte MCP — **le seul moyen normalisé** pour
5
- * qu'un agent apprenne où obtenir un jeton.
6
- *
7
- * ## Pourquoi un controller séparé
8
- *
9
- * Trois raisons, et aucune n'est esthétique. Le chemin vit **hors** du préfixe
10
- * `/nodefony` (la RFC impose `/.well-known/…` juste après l'hôte). Le document
11
- * est **public par conception** — un client doit pouvoir le lire avant d'avoir
12
- * le moindre jeton, donc il ne porte ni garde d'origine ni exigence de
13
- * localité, contrairement à la porte elle-même. Et il répond en `GET`, quand la
14
- * porte n'accepte que `POST`.
15
- *
16
- * ## Ce que publier coûte, et ce que ne rien publier coûte
17
- *
18
- * Le document ne révèle rien de sensible : l'émetteur des jetons, les scopes
19
- * compris, un nom. En revanche, ne PAS le publier a un coût mesuré — un client
20
- * qui ne le trouve pas suppose que le serveur d'autorisation est le serveur
21
- * lui-même, et part sonder des chemins qui n'existent pas. C'est exactement ce
22
- * qui a produit le bruit OAuth observé sur cette porte.
23
- *
24
- * 🔴 **Rôle éteint = `404`, jamais un document vide.** Un document sans serveur
25
- * d'autorisation apprendrait au client qu'un jeton est nécessaire sans jamais
26
- * lui dire où le demander : la spécification MCP l'interdit d'ailleurs
27
- * explicitement (« MUST include […] at least one authorization server »).
28
- */
29
- declare class OAuthMetadataController extends Controller {
30
- #private;
31
- constructor(context: ContextType);
32
- /**
33
- * `GET /.well-known/oauth-protected-resource/nodefony/mcp` — RFC 9728 §3.
34
- *
35
- * @returns le document JSON, ou `404` si cette porte n'est pas protégée
36
- */
37
- metadata(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
38
- }
39
- export default OAuthMetadataController;
@@ -1,66 +0,0 @@
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;
@@ -1,139 +0,0 @@
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;
@@ -1,48 +0,0 @@
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 {};
@@ -1,81 +0,0 @@
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>;