@lyrra/mcp-server 1.1.3 → 1.1.7

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 (108) hide show
  1. package/README.md +80 -250
  2. package/dist/auth-session.js +171 -0
  3. package/dist/eduflow-block-docs.js +438 -0
  4. package/dist/http-incoming-auth.js +48 -0
  5. package/dist/http-main.js +104 -0
  6. package/dist/index.js +16 -12
  7. package/dist/lyrra-http.js +80 -0
  8. package/dist/lyrra-mcp-core.js +174 -0
  9. package/dist/openapi-parse.js +61 -0
  10. package/dist/register-eduflow-block-tools.js +31 -0
  11. package/package.json +41 -13
  12. package/Dockerfile +0 -16
  13. package/dist/client.d.ts +0 -23
  14. package/dist/client.d.ts.map +0 -1
  15. package/dist/client.js +0 -92
  16. package/dist/client.js.map +0 -1
  17. package/dist/config.d.ts +0 -8
  18. package/dist/config.d.ts.map +0 -1
  19. package/dist/config.js +0 -8
  20. package/dist/config.js.map +0 -1
  21. package/dist/http-server.d.ts +0 -8
  22. package/dist/http-server.d.ts.map +0 -1
  23. package/dist/http-server.js +0 -481
  24. package/dist/http-server.js.map +0 -1
  25. package/dist/index.d.ts +0 -3
  26. package/dist/index.d.ts.map +0 -1
  27. package/dist/index.js.map +0 -1
  28. package/dist/resources/block-types.d.ts +0 -318
  29. package/dist/resources/block-types.d.ts.map +0 -1
  30. package/dist/resources/block-types.js +0 -297
  31. package/dist/resources/block-types.js.map +0 -1
  32. package/dist/resources/flow-schema.d.ts +0 -147
  33. package/dist/resources/flow-schema.d.ts.map +0 -1
  34. package/dist/resources/flow-schema.js +0 -143
  35. package/dist/resources/flow-schema.js.map +0 -1
  36. package/dist/server-factory.d.ts +0 -8
  37. package/dist/server-factory.d.ts.map +0 -1
  38. package/dist/server-factory.js +0 -82
  39. package/dist/server-factory.js.map +0 -1
  40. package/dist/tools/admin.d.ts +0 -265
  41. package/dist/tools/admin.d.ts.map +0 -1
  42. package/dist/tools/admin.js +0 -118
  43. package/dist/tools/admin.js.map +0 -1
  44. package/dist/tools/ai-designer.d.ts +0 -297
  45. package/dist/tools/ai-designer.d.ts.map +0 -1
  46. package/dist/tools/ai-designer.js +0 -89
  47. package/dist/tools/ai-designer.js.map +0 -1
  48. package/dist/tools/analytics.d.ts +0 -95
  49. package/dist/tools/analytics.d.ts.map +0 -1
  50. package/dist/tools/analytics.js +0 -44
  51. package/dist/tools/analytics.js.map +0 -1
  52. package/dist/tools/auth.d.ts +0 -61
  53. package/dist/tools/auth.d.ts.map +0 -1
  54. package/dist/tools/auth.js +0 -36
  55. package/dist/tools/auth.js.map +0 -1
  56. package/dist/tools/blocks.d.ts +0 -457
  57. package/dist/tools/blocks.d.ts.map +0 -1
  58. package/dist/tools/blocks.js +0 -173
  59. package/dist/tools/blocks.js.map +0 -1
  60. package/dist/tools/connections.d.ts +0 -173
  61. package/dist/tools/connections.d.ts.map +0 -1
  62. package/dist/tools/connections.js +0 -81
  63. package/dist/tools/connections.js.map +0 -1
  64. package/dist/tools/eduflow.d.ts +0 -409
  65. package/dist/tools/eduflow.d.ts.map +0 -1
  66. package/dist/tools/eduflow.js +0 -139
  67. package/dist/tools/eduflow.js.map +0 -1
  68. package/dist/tools/participants.d.ts +0 -221
  69. package/dist/tools/participants.d.ts.map +0 -1
  70. package/dist/tools/participants.js +0 -70
  71. package/dist/tools/participants.js.map +0 -1
  72. package/dist/tools/presentation.d.ts +0 -233
  73. package/dist/tools/presentation.d.ts.map +0 -1
  74. package/dist/tools/presentation.js +0 -57
  75. package/dist/tools/presentation.js.map +0 -1
  76. package/dist/tools/projects.d.ts +0 -131
  77. package/dist/tools/projects.d.ts.map +0 -1
  78. package/dist/tools/projects.js +0 -55
  79. package/dist/tools/projects.js.map +0 -1
  80. package/dist/tools/resources.d.ts +0 -93
  81. package/dist/tools/resources.d.ts.map +0 -1
  82. package/dist/tools/resources.js +0 -37
  83. package/dist/tools/resources.js.map +0 -1
  84. package/dist/tools/store.d.ts +0 -125
  85. package/dist/tools/store.d.ts.map +0 -1
  86. package/dist/tools/store.js +0 -66
  87. package/dist/tools/store.js.map +0 -1
  88. package/mcp-config.example.json +0 -14
  89. package/src/client.ts +0 -106
  90. package/src/config.ts +0 -7
  91. package/src/http-server.ts +0 -591
  92. package/src/index.ts +0 -23
  93. package/src/resources/block-types.ts +0 -298
  94. package/src/resources/flow-schema.ts +0 -148
  95. package/src/server-factory.ts +0 -109
  96. package/src/tools/admin.ts +0 -128
  97. package/src/tools/ai-designer.ts +0 -97
  98. package/src/tools/analytics.ts +0 -49
  99. package/src/tools/auth.ts +0 -39
  100. package/src/tools/blocks.ts +0 -186
  101. package/src/tools/connections.ts +0 -83
  102. package/src/tools/eduflow.ts +0 -150
  103. package/src/tools/participants.ts +0 -77
  104. package/src/tools/presentation.ts +0 -61
  105. package/src/tools/projects.ts +0 -61
  106. package/src/tools/resources.ts +0 -41
  107. package/src/tools/store.ts +0 -67
  108. package/tsconfig.json +0 -19
package/README.md CHANGED
@@ -1,34 +1,21 @@
1
- # LYRRA Studio MCP Server
1
+ # Lyrra Studio MCP server
2
2
 
3
- Serveur MCP (Model Context Protocol) pour piloter **LYRRA Studio** depuis un outil IA externe (Claude Desktop, Claude.ai, Cursor, Copilot CLI, etc.).
3
+ **stdio** process (Model Context Protocol): one tool per backend **OpenAPI** operation, plus `lyrra_meta`, `lyrra_search_operations`, and `lyrra://…` resources.
4
4
 
5
- ## Modes de transport
5
+ ## Prerequisites
6
6
 
7
- ### Mode stdio (Claude Desktop, Cursor)
8
- Transport stdio classique le serveur MCP tourne en processus local.
7
+ - Node.js 20
8
+ - Reachable Lyrra backend with generated `openapi/openapi.json` (`npm run openapi:generate` in `apps/backend`)
9
9
 
10
- ### Mode HTTP (Claude.ai, applications web)
11
- Transport HTTP Streamable avec OAuth 2.0 — le serveur MCP tourne comme service web accessible via `https://lyrrastudio.com/mcp`.
10
+ ## Install from npm (Claude / Cursor)
12
11
 
13
- ## Installation
12
+ Published as **`@lyrra/mcp-server`**. No local clone required:
14
13
 
15
14
  ```bash
16
- npm install -g @lyrra/mcp-server
15
+ npx -y @lyrra/mcp-server
17
16
  ```
18
17
 
19
- Ou utilisez directement avec `npx` (aucune installation requise) — voir la configuration ci-dessous.
20
-
21
- ## Configuration
22
-
23
- ### 1. Obtenir vos identifiants
24
-
25
- Connectez-vous à LYRRA Studio → Dashboard Entreprise → Serveur MCP → Onglet "Identifiants" → Créer un nouveau client.
26
-
27
- Vous obtiendrez un **Client ID** et un **Client Secret** (affiché une seule fois).
28
-
29
- ### 2. Claude Desktop (mode stdio)
30
-
31
- Ajoutez dans `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) ou `%APPDATA%\Claude\claude_desktop_config.json` (Windows) :
18
+ In **Claude Desktop** (`claude_desktop_config.json`), prefer:
32
19
 
33
20
  ```json
34
21
  {
@@ -37,257 +24,100 @@ Ajoutez dans `~/Library/Application Support/Claude/claude_desktop_config.json` (
37
24
  "command": "npx",
38
25
  "args": ["-y", "@lyrra/mcp-server"],
39
26
  "env": {
40
- "LYRRA_CLIENT_ID": "votre_client_id",
41
- "LYRRA_CLIENT_SECRET": "rak_votre_client_secret",
42
- "LYRRA_API_URL": "https://lyrrastudio.com/api",
43
- "LYRRA_EDUFLOW_API_URL": "https://lyrrastudio.com/api/eduflow"
27
+ "LYRRA_API_URL": "https://your-domain.com/api",
28
+ "LYRRA_CLIENT_ID": "rak_xxxxxxxx",
29
+ "LYRRA_CLIENT_SECRET": "rak_xxxxxxxx_yyyyyyyy_zzzzzzzzzzzzzzzz"
44
30
  }
45
31
  }
46
32
  }
47
33
  }
48
34
  ```
49
35
 
50
- > Après modification, quittez complètement Claude Desktop (Cmd+Q / Ctrl+Q) et relancez-le.
36
+ Use **Header Auth** keys from the institution dashboard instead of client id/secret:
51
37
 
52
- ### 3. Claude.ai (mode HTTP)
38
+ ```json
39
+ "env": {
40
+ "LYRRA_API_URL": "https://your-domain.com/api",
41
+ "LYRRA_MCP_HEADER_NAME": "X-Lyrra-Api-Key",
42
+ "LYRRA_MCP_HEADER_VALUE": "rak_…full secret…"
43
+ }
44
+ ```
53
45
 
54
- Ajoutez le MCP serveur dans Claude.ai Settings Integrations Add MCP Server :
46
+ Global install (optional): `npm install -g @lyrra/mcp-server` then run **`lyrra-mcp`** (binary name on `PATH`).
55
47
 
56
- - **URL** : `https://lyrrastudio.com/mcp`
48
+ ## Streamable HTTP / n8n (`https://…/mcp`)
57
49
 
58
- Claude.ai lancera automatiquement le flux OAuth :
59
- 1. Vous serez redirigé vers la page d'autorisation LYRRA Studio
60
- 2. Entrez votre **Client Secret** (clé API `rak_...`)
61
- 3. L'accès est autorisé, Claude.ai peut utiliser vos outils MCP
50
+ Clients that need an **HTTPS URL** (e.g. n8n **MCP Client**) use the [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) transport on path **`/mcp`**.
62
51
 
63
- ### 4. Cursor (mode stdio)
52
+ - **Docker:** enable service **`mcp-http`** in `docker-compose.yml` and Nginx **`location /mcp`** (see `apps/frontend/nginx.default.conf`).
53
+ - **Auth:** n8n **Header Auth** — same header name and **full** secret as an institution **Header auth** key (e.g. `X-Lyrra-Api-Key` + `rak_…`).
54
+ - **Run locally:** `LYRRA_API_URL=http://localhost:3001/api npm run start:http` → listens on **`LYRRA_MCP_HTTP_PORT`** (default **3457**), path `/mcp`.
64
55
 
65
- Ajoutez dans `.cursor/mcp.json` :
56
+ Binaries after global install: **`lyrra-mcp`** (stdio) and **`lyrra-mcp-http`** (HTTP).
66
57
 
67
- ```json
68
- {
69
- "mcpServers": {
70
- "lyrra-studio": {
71
- "command": "npx",
72
- "args": ["-y", "@lyrra/mcp-server"],
73
- "env": {
74
- "LYRRA_CLIENT_ID": "votre_client_id",
75
- "LYRRA_CLIENT_SECRET": "rak_votre_client_secret",
76
- "LYRRA_API_URL": "https://lyrrastudio.com/api",
77
- "LYRRA_EDUFLOW_API_URL": "https://lyrrastudio.com/api/eduflow"
78
- }
79
- }
80
- }
81
- }
58
+ ## Develop from this monorepo
59
+
60
+ ```bash
61
+ cd lyrra-studio-app/apps/mcp-server
62
+ npm ci
63
+ npm run build
82
64
  ```
83
65
 
84
- ### 5. GitHub Copilot CLI / VS Code
66
+ ### Publish a new version (maintainers)
85
67
 
86
- Ajoutez dans `.vscode/mcp.json` ou les settings Copilot :
68
+ 1. Create an **npm** organization or user scope **`@lyrra`** and log in: `npm login`.
69
+ 2. Bump **`version`** in `package.json` (semver).
70
+ 3. From `apps/mcp-server`: `npm publish`
71
+ (`publishConfig.access` is `public` for the scoped package.)
87
72
 
88
- ```json
89
- {
90
- "mcpServers": {
91
- "lyrra-studio": {
92
- "command": "npx",
93
- "args": ["-y", "@lyrra/mcp-server"],
94
- "env": {
95
- "LYRRA_CLIENT_ID": "votre_client_id",
96
- "LYRRA_CLIENT_SECRET": "rak_votre_client_secret",
97
- "LYRRA_API_URL": "https://lyrrastudio.com/api",
98
- "LYRRA_EDUFLOW_API_URL": "https://lyrrastudio.com/api/eduflow"
99
- }
100
- }
101
- }
102
- }
103
- ```
73
+ Ensure `dist/` is built (`prepublishOnly` runs `npm run build` automatically).
104
74
 
105
- ## Variables d'environnement
106
-
107
- | Variable | Défaut | Description |
108
- |---|---|---|
109
- | `LYRRA_CLIENT_ID` | - | Client ID (nom du client) |
110
- | `LYRRA_CLIENT_SECRET` | - | Client Secret (clé secrète, préfixe `rak_`) |
111
- | `LYRRA_API_URL` | `http://localhost:3001/api` | URL de l'API REST |
112
- | `LYRRA_EDUFLOW_API_URL` | `http://localhost:3001/api/eduflow` | URL de l'API EduFlow |
113
-
114
- ## Tools disponibles (58)
115
-
116
- ### 🔐 Authentification
117
- | Tool | Description |
118
- |---|---|
119
- | `auth_login` | Se connecter avec email/mot de passe |
120
- | `auth_get_profile` | Profil utilisateur connecté |
121
- | `auth_list_api_keys` | Lister les clés API |
122
-
123
- ### 📚 Parcours EduFlow
124
- | Tool | Description |
125
- |---|---|
126
- | `eduflow_list` | Lister tous les parcours |
127
- | `eduflow_get` | Détails d'un parcours |
128
- | `eduflow_create` | Créer un parcours |
129
- | `eduflow_update` | Mettre à jour un parcours |
130
- | `eduflow_delete` | Supprimer un parcours |
131
- | `eduflow_duplicate` | Dupliquer un parcours |
132
- | `eduflow_change_status` | Changer le statut (draft/published/archived) |
133
- | `eduflow_export` | Exporter un parcours |
134
- | `eduflow_get_public` | Infos publiques d'un parcours |
135
- | `eduflow_get_urls` | Obtenir les liens (prévisualisation, édition, analytics, etc.) |
136
-
137
- ### 🧱 Blocs
138
- | Tool | Description |
139
- |---|---|
140
- | `block_list_types` | Documentation des 29 types de blocs |
141
- | `block_get` | Détails d'un bloc |
142
- | `block_create` | Créer un bloc |
143
- | `block_update` | Mettre à jour un bloc |
144
- | `block_batch_update` | Mise à jour en lot |
145
- | `block_delete` | Supprimer un bloc |
146
- | `block_generate_tts` | Générer audio TTS |
147
-
148
- ### 🔗 Connexions
149
- | Tool | Description |
150
- |---|---|
151
- | `connection_list` | Lister les connexions |
152
- | `connection_add` | Ajouter une connexion |
153
- | `connection_remove` | Supprimer une connexion |
154
-
155
- ### 👥 Participants
156
- | Tool | Description |
157
- |---|---|
158
- | `participant_list` | Lister les participants |
159
- | `participant_add` | Ajouter des participants |
160
- | `participant_remove` | Retirer un participant |
161
- | `participant_get_progress` | Progression d'un participant |
162
- | `participant_get_overview` | Vue d'ensemble participant |
163
- | `participant_get_flow_stats` | Stats de tous les participants |
164
-
165
- ### 📊 Analytics
166
- | Tool | Description |
167
- |---|---|
168
- | `analytics_overview` | Dashboard global |
169
- | `analytics_flow_learners` | Stats apprenants par parcours |
170
- | `analytics_flow_dashboard` | Dashboard analytics parcours |
171
- | `analytics_my_stats` | Mes statistiques |
172
-
173
- ### 🤖 AI Designer
174
- | Tool | Description |
175
- |---|---|
176
- | `ai_generate_plan` | Générer un plan de parcours |
177
- | `ai_generate_block` | Générer le contenu d'un bloc |
178
- | `ai_generate_presentation` | Générer la page de présentation |
179
- | `ai_generate_objectives` | Générer les objectifs pédagogiques |
180
- | `ai_save_objectives` | Sauvegarder les objectifs |
181
- | `ai_chat_message` | Discuter avec le designer IA |
182
- | `ai_get_conversation` | Historique de conversation IA |
183
-
184
- ### 🎭 Présentation
185
- | Tool | Description |
186
- |---|---|
187
- | `presentation_get` | Page de présentation |
188
- | `presentation_update` | Mettre à jour la présentation |
189
- | `presentation_toggle` | Activer/désactiver |
190
-
191
- ### 🏪 Store
192
- | Tool | Description |
193
- |---|---|
194
- | `store_list_audiobooks` | Lister les audiobooks |
195
- | `store_get_audiobook` | Détails d'un audiobook |
196
- | `store_list_authors` | Auteurs en vedette |
197
- | `store_my_library` | Ma bibliothèque |
198
- | `store_search` | Rechercher dans le store |
199
-
200
- ### 🎵 Projets Audio
201
- | Tool | Description |
202
- |---|---|
203
- | `project_list` | Lister mes projets |
204
- | `project_get` | Détails d'un projet |
205
- | `project_create` | Créer un projet |
206
- | `project_update` | Mettre à jour un projet |
207
- | `project_delete` | Supprimer un projet |
208
-
209
- ### 📁 Ressources
210
- | Tool | Description |
211
- |---|---|
212
- | `resource_list` | Lister les ressources |
213
- | `resource_delete` | Supprimer une ressource |
214
- | `resource_list_categories` | Catégories disponibles |
215
-
216
- ### 🔧 Versions, Gamification & Webhooks
217
- | Tool | Description |
218
- |---|---|
219
- | `version_list` | Versions d'un parcours |
220
- | `version_create` | Créer une version |
221
- | `version_activate` | Activer une version |
222
- | `gamification_stats` | Stats de gamification |
223
- | `gamification_objectives` | Objectifs à atteindre |
224
- | `activity_history` | Historique d'activité |
225
- | `activity_stats` | Stats d'utilisation |
226
- | `webhook_list` | Lister les webhooks |
227
- | `webhook_create` | Créer un webhook |
228
- | `webhook_delete` | Supprimer un webhook |
229
- | `webhook_test` | Tester un webhook |
230
-
231
- ## Resources MCP
232
-
233
- | URI | Description |
234
- |---|---|
235
- | `lyrra://block-types` | Documentation complète des 29 types de blocs |
236
- | `lyrra://flow-construction-guide` | Guide de construction de parcours |
237
-
238
- ## Exemples d'utilisation avec Claude
239
-
240
- ### Créer un parcours complet
241
- > "Crée un parcours EduFlow sur les fractions pour des élèves de CM2, avec 3 leçons en texte, un quiz après chaque leçon, et un certificat de réussite à la fin."
242
-
243
- ### Analyser les statistiques
244
- > "Montre-moi les statistiques de mon parcours 'Introduction au Python'. Quels sont les blocs où les étudiants passent le plus de temps ?"
245
-
246
- ### Modifier un parcours existant
247
- > "Dans mon parcours sur la photosynthèse, ajoute un bloc vidéo après le texte d'introduction et connecte-le au quiz."
248
-
249
- ## Développement
75
+ **Publish error:** `Cannot implicitly apply the "latest" tag because previously published version … is higher` — the registry already has a **newer** semver on `latest` (e.g. `1.1.3`). Bump `package.json` to something **greater** (e.g. `1.1.4`), not `1.0.x`. Alternatively: `npm publish --tag maintenance` to publish an older line without moving `latest`.
250
76
 
251
- ```bash
252
- # Lancer en mode stdio (développement local)
253
- npm run dev
77
+ Run `npm pkg fix` in this folder to apply npm’s suggested `package.json` fixes (`bin`, `repository`, …).
254
78
 
255
- # Lancer en mode HTTP (développement local)
256
- npm run dev:http
79
+ ## Environment variables
257
80
 
258
- # Rebuild après modifications
259
- npm run build
81
+ | Variable | Purpose |
82
+ |----------|---------|
83
+ | `LYRRA_API_URL` | API base, e.g. `https://yourdomain/api` or `http://localhost:3001/api` |
84
+ | `LYRRA_CLIENT_ID` | Key prefix (`keyPrefix`) |
85
+ | `LYRRA_CLIENT_SECRET` | Full secret `rak_…` |
86
+ | `LYRRA_ACCESS_TOKEN` | *(optional)* Bearer JWT if not using key exchange |
87
+ | `LYRRA_MCP_HEADER_NAME` | *(optional)* HTTP header name for **Header Auth** keys (e.g. `X-Lyrra-Api-Key`; alias `LYRRA_HEADER_AUTH_NAME`) |
88
+ | `LYRRA_MCP_HEADER_VALUE` | *(optional)* Same secret as shown once at key creation (alias `LYRRA_MCP_HEADER_SECRET` or `LYRRA_HEADER_AUTH_VALUE`) |
89
+ | `LYRRA_OPENAPI_URL` | *(optional)* OpenAPI JSON URL |
90
+ | `LYRRA_MCP_MAX_TOOLS` | *(optional)* Max number of tools (integer) |
91
+ | `LYRRA_MCP_HTTP_PORT` | *(HTTP mode only)* Listen port (default `3457`) |
92
+ | `LYRRA_MCP_SKIP_AUTH_VALIDATE` | *(optional)* Set to `1` to skip `GET /api/auth/me` on each MCP request (insecure; dev only) |
93
+ | `LYRRA_MCP_EXTRA_INBOUND_HEADERS` | *(HTTP mode)* Comma-separated extra header names to copy from the MCP request into Lyrra API calls |
94
+
95
+ Client ID + Secret → JWT exchange uses `POST /api/auth/api-key/token`. **Header Auth** keys do not use that route: send the header on each API request instead (same as n8n MCP « Header Auth »).
96
+
97
+ ## EduFlow block documentation
260
98
 
261
- # Lancer en production (stdio)
99
+ - Tool **`lyrra_eduflow_blocks_index`**: list of documented types.
100
+ - One tool per type: **`lyrra_eduflow_block_<type>`** (e.g. `lyrra_eduflow_block_quiz_mcq`) — role, `data` / `settings` fields, graph, persistence.
101
+ - Sheet source: `src/eduflow-block-docs.ts` (keep aligned with the React designer).
102
+
103
+ ## Run
104
+
105
+ ```bash
262
106
  npm start
107
+ ```
108
+
109
+ Development (no pre-build): `npm run dev`
110
+
111
+ ## Automated test (no Lyrra backend)
263
112
 
264
- # Lancer en production (HTTP)
265
- npm run start:http
113
+ A minimal OpenAPI is served locally; the script checks `initialize`, `tools/list`, and a `tools/call` on a block sheet:
114
+
115
+ ```bash
116
+ npm run test:mcp
266
117
  ```
267
118
 
268
- ## Architecture HTTP
269
-
270
- Le mode HTTP expose les endpoints suivants :
271
-
272
- | Endpoint | Méthode | Description |
273
- |---|---|---|
274
- | `/.well-known/oauth-protected-resource/mcp` | GET | Métadonnées RFC 9728 |
275
- | `/mcp/.well-known/oauth-authorization-server` | GET | Métadonnées OAuth AS |
276
- | `/mcp/register` | POST | Enregistrement dynamique de client OAuth |
277
- | `/mcp/authorize` | GET | Page d'autorisation OAuth |
278
- | `/mcp/approve` | POST | Validation des identifiants |
279
- | `/mcp/token` | POST | Échange de code → token |
280
- | `/mcp/revoke` | POST | Révocation de token |
281
- | `/mcp` | POST | Protocole MCP (messages) |
282
- | `/mcp` | GET | SSE stream (notifications serveur) |
283
- | `/mcp` | DELETE | Fermeture de session |
284
- | `/mcp/health` | GET | Health check |
285
-
286
- ### Variables d'environnement (mode HTTP)
287
-
288
- | Variable | Défaut | Description |
289
- |---|---|---|
290
- | `MCP_HTTP_PORT` | `3002` | Port du serveur HTTP |
291
- | `MCP_BASE_URL` | `https://lyrrastudio.com` | URL publique de base |
292
- | `LYRRA_API_URL` | `http://localhost:3001/api` | URL de l'API backend |
293
- | `LYRRA_EDUFLOW_API_URL` | `http://localhost:3001/api/eduflow` | URL de l'API EduFlow |
119
+ ## MCP client (local build vs npm)
120
+
121
+ - **Recommended:** `command` `npx`, `args` `["-y", "@lyrra/mcp-server"]` (see above).
122
+ - **Local monorepo:** `command` `node`, `args`: **absolute** path to `apps/mcp-server/dist/index.js`
123
+ `env`: `LYRRA_API_URL`, `LYRRA_CLIENT_ID`, `LYRRA_CLIENT_SECRET` (or `LYRRA_ACCESS_TOKEN` / header variables).
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Access token for Lyrra API calls.
3
+ * - LYRRA_ACCESS_TOKEN: JWT directly
4
+ * - or LYRRA_CLIENT_ID (keyPrefix) + LYRRA_CLIENT_SECRET (rak_… secret) → POST /api/auth/api-key/token
5
+ * - or clé institution « header_auth » : LYRRA_MCP_HEADER_NAME + LYRRA_MCP_HEADER_VALUE
6
+ * (alias : LYRRA_HEADER_AUTH_NAME / LYRRA_HEADER_AUTH_VALUE) — même en-tête que n8n « Header Auth »
7
+ * - or (HTTP MCP) en-têtes par requête via runWithInboundAuthHeaders (n8n Header Auth sur /mcp)
8
+ */
9
+ import { AsyncLocalStorage } from 'node:async_hooks';
10
+ const inboundAuthAls = new AsyncLocalStorage();
11
+ /** Exécute une callback avec des en-têtes d’auth injectés pour les appels API (transport MCP HTTP). */
12
+ export function runWithInboundAuthHeaders(headers, fn) {
13
+ return inboundAuthAls.run(headers, fn);
14
+ }
15
+ let cached = null;
16
+ function apiBase() {
17
+ const u = process.env.LYRRA_API_URL?.trim();
18
+ if (!u) {
19
+ throw new Error('LYRRA_API_URL is required (e.g. https://lyrrastudio.com/api or http://localhost:3001/api)');
20
+ }
21
+ return u.replace(/\/+$/, '');
22
+ }
23
+ /** HTTP origin without /api suffix, for /api/auth/… and /api/openapi.json */
24
+ export function requestOrigin() {
25
+ const base = apiBase();
26
+ if (base.endsWith('/api')) {
27
+ const o = base.slice(0, -4);
28
+ return o.length > 0 ? o : base;
29
+ }
30
+ return base;
31
+ }
32
+ function parseJsonEnvelope(json) {
33
+ if (!json || typeof json !== 'object')
34
+ return {};
35
+ const o = json;
36
+ const data = o.data;
37
+ if (data && typeof data === 'object')
38
+ return data;
39
+ return o;
40
+ }
41
+ async function exchangeApiKey(prefix, secret) {
42
+ const origin = requestOrigin();
43
+ const url = `${origin}/api/auth/api-key/token`;
44
+ const r = await fetch(url, {
45
+ method: 'POST',
46
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
47
+ body: JSON.stringify({ keyPrefix: prefix, secret }),
48
+ });
49
+ const text = await r.text();
50
+ if (!r.ok) {
51
+ throw new Error(`API key exchange failed (${r.status}): ${text.slice(0, 800)}`);
52
+ }
53
+ let parsed;
54
+ try {
55
+ parsed = JSON.parse(text);
56
+ }
57
+ catch {
58
+ throw new Error('Invalid auth response (expected JSON)');
59
+ }
60
+ const body = parseJsonEnvelope(parsed);
61
+ const access = typeof body.accessToken === 'string' ? body.accessToken : '';
62
+ const refresh = typeof body.refreshToken === 'string' ? body.refreshToken : '';
63
+ if (!access) {
64
+ throw new Error('Auth response missing accessToken');
65
+ }
66
+ let expMs = Date.now() + 10 * 60_000;
67
+ try {
68
+ const part = access.split('.')[1];
69
+ if (part) {
70
+ const payload = JSON.parse(Buffer.from(part, 'base64url').toString('utf8'));
71
+ if (typeof payload.exp === 'number') {
72
+ expMs = payload.exp * 1000;
73
+ }
74
+ }
75
+ }
76
+ catch {
77
+ /* keep default expMs */
78
+ }
79
+ cached = { access, refresh, expMs };
80
+ }
81
+ async function refreshAccess() {
82
+ if (!cached?.refresh)
83
+ return false;
84
+ const origin = requestOrigin();
85
+ const r = await fetch(`${origin}/api/auth/refresh`, {
86
+ method: 'POST',
87
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
88
+ body: JSON.stringify({ refreshToken: cached.refresh }),
89
+ });
90
+ const text = await r.text();
91
+ if (!r.ok)
92
+ return false;
93
+ try {
94
+ const parsed = JSON.parse(text);
95
+ const body = parseJsonEnvelope(parsed);
96
+ const access = typeof body.accessToken === 'string' ? body.accessToken : '';
97
+ if (!access)
98
+ return false;
99
+ let expMs = Date.now() + 10 * 60_000;
100
+ try {
101
+ const part = access.split('.')[1];
102
+ if (part) {
103
+ const payload = JSON.parse(Buffer.from(part, 'base64url').toString('utf8'));
104
+ if (typeof payload.exp === 'number')
105
+ expMs = payload.exp * 1000;
106
+ }
107
+ }
108
+ catch {
109
+ /* ignore */
110
+ }
111
+ cached = { ...cached, access, expMs };
112
+ return true;
113
+ }
114
+ catch {
115
+ return false;
116
+ }
117
+ }
118
+ function headerAuthFromEnv() {
119
+ const name = process.env.LYRRA_MCP_HEADER_NAME?.trim() ||
120
+ process.env.LYRRA_HEADER_AUTH_NAME?.trim() ||
121
+ '';
122
+ const value = process.env.LYRRA_MCP_HEADER_SECRET?.trim() ||
123
+ process.env.LYRRA_MCP_HEADER_VALUE?.trim() ||
124
+ process.env.LYRRA_HEADER_AUTH_VALUE?.trim() ||
125
+ '';
126
+ if (!name || !value)
127
+ return null;
128
+ return { name, value };
129
+ }
130
+ /**
131
+ * En-têtes à envoyer sur chaque appel API Lyrra (Bearer ou clé header_auth).
132
+ */
133
+ export async function getLyrraRequestAuthHeaders() {
134
+ const inbound = inboundAuthAls.getStore();
135
+ if (inbound && Object.keys(inbound).length > 0) {
136
+ return { ...inbound };
137
+ }
138
+ const headerPair = headerAuthFromEnv();
139
+ if (headerPair) {
140
+ return { [headerPair.name]: headerPair.value };
141
+ }
142
+ const token = await getAccessToken();
143
+ return { Authorization: `Bearer ${token}` };
144
+ }
145
+ export async function getAccessToken() {
146
+ if (headerAuthFromEnv()) {
147
+ throw new Error('Header auth is configured (LYRRA_MCP_HEADER_*); use getLyrraRequestAuthHeaders for API calls');
148
+ }
149
+ const direct = process.env.LYRRA_ACCESS_TOKEN?.trim();
150
+ if (direct)
151
+ return direct;
152
+ const prefix = process.env.LYRRA_CLIENT_ID?.trim();
153
+ const secret = process.env.LYRRA_CLIENT_SECRET?.trim();
154
+ if (!prefix || !secret) {
155
+ throw new Error('Set LYRRA_ACCESS_TOKEN (JWT), LYRRA_CLIENT_ID + LYRRA_CLIENT_SECRET, or LYRRA_MCP_HEADER_NAME + LYRRA_MCP_HEADER_VALUE');
156
+ }
157
+ const margin = 90_000;
158
+ if (cached && cached.expMs > Date.now() + margin) {
159
+ return cached.access;
160
+ }
161
+ if (cached?.refresh && (await refreshAccess())) {
162
+ return cached.access;
163
+ }
164
+ cached = null;
165
+ await exchangeApiKey(prefix, secret);
166
+ return cached.access;
167
+ }
168
+ /** Clear cache (e.g. after persistent 401) */
169
+ export function clearTokenCache() {
170
+ cached = null;
171
+ }