@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.
- package/README.md +80 -250
- package/dist/auth-session.js +171 -0
- package/dist/eduflow-block-docs.js +438 -0
- package/dist/http-incoming-auth.js +48 -0
- package/dist/http-main.js +104 -0
- package/dist/index.js +16 -12
- package/dist/lyrra-http.js +80 -0
- package/dist/lyrra-mcp-core.js +174 -0
- package/dist/openapi-parse.js +61 -0
- package/dist/register-eduflow-block-tools.js +31 -0
- package/package.json +41 -13
- package/Dockerfile +0 -16
- package/dist/client.d.ts +0 -23
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -92
- package/dist/client.js.map +0 -1
- package/dist/config.d.ts +0 -8
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -8
- package/dist/config.js.map +0 -1
- package/dist/http-server.d.ts +0 -8
- package/dist/http-server.d.ts.map +0 -1
- package/dist/http-server.js +0 -481
- package/dist/http-server.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/resources/block-types.d.ts +0 -318
- package/dist/resources/block-types.d.ts.map +0 -1
- package/dist/resources/block-types.js +0 -297
- package/dist/resources/block-types.js.map +0 -1
- package/dist/resources/flow-schema.d.ts +0 -147
- package/dist/resources/flow-schema.d.ts.map +0 -1
- package/dist/resources/flow-schema.js +0 -143
- package/dist/resources/flow-schema.js.map +0 -1
- package/dist/server-factory.d.ts +0 -8
- package/dist/server-factory.d.ts.map +0 -1
- package/dist/server-factory.js +0 -82
- package/dist/server-factory.js.map +0 -1
- package/dist/tools/admin.d.ts +0 -265
- package/dist/tools/admin.d.ts.map +0 -1
- package/dist/tools/admin.js +0 -118
- package/dist/tools/admin.js.map +0 -1
- package/dist/tools/ai-designer.d.ts +0 -297
- package/dist/tools/ai-designer.d.ts.map +0 -1
- package/dist/tools/ai-designer.js +0 -89
- package/dist/tools/ai-designer.js.map +0 -1
- package/dist/tools/analytics.d.ts +0 -95
- package/dist/tools/analytics.d.ts.map +0 -1
- package/dist/tools/analytics.js +0 -44
- package/dist/tools/analytics.js.map +0 -1
- package/dist/tools/auth.d.ts +0 -61
- package/dist/tools/auth.d.ts.map +0 -1
- package/dist/tools/auth.js +0 -36
- package/dist/tools/auth.js.map +0 -1
- package/dist/tools/blocks.d.ts +0 -457
- package/dist/tools/blocks.d.ts.map +0 -1
- package/dist/tools/blocks.js +0 -173
- package/dist/tools/blocks.js.map +0 -1
- package/dist/tools/connections.d.ts +0 -173
- package/dist/tools/connections.d.ts.map +0 -1
- package/dist/tools/connections.js +0 -81
- package/dist/tools/connections.js.map +0 -1
- package/dist/tools/eduflow.d.ts +0 -409
- package/dist/tools/eduflow.d.ts.map +0 -1
- package/dist/tools/eduflow.js +0 -139
- package/dist/tools/eduflow.js.map +0 -1
- package/dist/tools/participants.d.ts +0 -221
- package/dist/tools/participants.d.ts.map +0 -1
- package/dist/tools/participants.js +0 -70
- package/dist/tools/participants.js.map +0 -1
- package/dist/tools/presentation.d.ts +0 -233
- package/dist/tools/presentation.d.ts.map +0 -1
- package/dist/tools/presentation.js +0 -57
- package/dist/tools/presentation.js.map +0 -1
- package/dist/tools/projects.d.ts +0 -131
- package/dist/tools/projects.d.ts.map +0 -1
- package/dist/tools/projects.js +0 -55
- package/dist/tools/projects.js.map +0 -1
- package/dist/tools/resources.d.ts +0 -93
- package/dist/tools/resources.d.ts.map +0 -1
- package/dist/tools/resources.js +0 -37
- package/dist/tools/resources.js.map +0 -1
- package/dist/tools/store.d.ts +0 -125
- package/dist/tools/store.d.ts.map +0 -1
- package/dist/tools/store.js +0 -66
- package/dist/tools/store.js.map +0 -1
- package/mcp-config.example.json +0 -14
- package/src/client.ts +0 -106
- package/src/config.ts +0 -7
- package/src/http-server.ts +0 -591
- package/src/index.ts +0 -23
- package/src/resources/block-types.ts +0 -298
- package/src/resources/flow-schema.ts +0 -148
- package/src/server-factory.ts +0 -109
- package/src/tools/admin.ts +0 -128
- package/src/tools/ai-designer.ts +0 -97
- package/src/tools/analytics.ts +0 -49
- package/src/tools/auth.ts +0 -39
- package/src/tools/blocks.ts +0 -186
- package/src/tools/connections.ts +0 -83
- package/src/tools/eduflow.ts +0 -150
- package/src/tools/participants.ts +0 -77
- package/src/tools/presentation.ts +0 -61
- package/src/tools/projects.ts +0 -61
- package/src/tools/resources.ts +0 -41
- package/src/tools/store.ts +0 -67
- package/tsconfig.json +0 -19
package/README.md
CHANGED
|
@@ -1,34 +1,21 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Lyrra Studio MCP server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**stdio** process (Model Context Protocol): one tool per backend **OpenAPI** operation, plus `lyrra_meta`, `lyrra_search_operations`, and `lyrra://…` resources.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Prerequisites
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
- Node.js ≥ 20
|
|
8
|
+
- Reachable Lyrra backend with generated `openapi/openapi.json` (`npm run openapi:generate` in `apps/backend`)
|
|
9
9
|
|
|
10
|
-
|
|
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
|
-
|
|
12
|
+
Published as **`@lyrra/mcp-server`**. No local clone required:
|
|
14
13
|
|
|
15
14
|
```bash
|
|
16
|
-
|
|
15
|
+
npx -y @lyrra/mcp-server
|
|
17
16
|
```
|
|
18
17
|
|
|
19
|
-
|
|
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
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
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
|
-
|
|
36
|
+
Use **Header Auth** keys from the institution dashboard instead of client id/secret:
|
|
51
37
|
|
|
52
|
-
|
|
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
|
-
|
|
46
|
+
Global install (optional): `npm install -g @lyrra/mcp-server` then run **`lyrra-mcp`** (binary name on `PATH`).
|
|
55
47
|
|
|
56
|
-
|
|
48
|
+
## Streamable HTTP / n8n (`https://…/mcp`)
|
|
57
49
|
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
+
Binaries after global install: **`lyrra-mcp`** (stdio) and **`lyrra-mcp-http`** (HTTP).
|
|
66
57
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
###
|
|
66
|
+
### Publish a new version (maintainers)
|
|
85
67
|
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
256
|
-
npm run dev:http
|
|
79
|
+
## Environment variables
|
|
257
80
|
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
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
|
-
|
|
265
|
-
|
|
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
|
-
##
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
+
}
|