@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
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP documentation for each EduFlow block type (aligned with the React designer).
|
|
3
|
+
* Types: apps/frontend/src/types/eduflow.ts, palette apps/frontend/src/lib/eduflow-designer-palette-blueprint.ts
|
|
4
|
+
*/
|
|
5
|
+
export const EDUFLOW_BLOCK_COMMON = `## Shared block data (JSON model)
|
|
6
|
+
|
|
7
|
+
Each block in \`EduFlow.blocks[]\` includes:
|
|
8
|
+
- \`id\` (UUID), \`type\` (string, e.g. \`quiz_mcq\`), \`position\`, \`connections\` (graph in/out)
|
|
9
|
+
- optional \`isRequired\`, \`isLocked\` (institution-locked block), \`order\`, \`label\`, \`color\`, \`metadata\`
|
|
10
|
+
|
|
11
|
+
**Object \`data\` (\`BlockData\`)** — shared:
|
|
12
|
+
- \`title\`, \`description\` (rich HTML in the designer)
|
|
13
|
+
- \`descriptionAudioUrl\`: TTS URL for the description (if generated)
|
|
14
|
+
- \`content\`: text or HTML depending on type (e.g. question stem)
|
|
15
|
+
- \`mediaUrl\`: primary media when applicable
|
|
16
|
+
- \`settings\`: \`Record<string, unknown>\` — **type-specific parameters** (see per-block sections below)
|
|
17
|
+
- \`scoring\`: max points, pass threshold, method (\`automatic\` | \`manual\` | \`peer\` | \`ai\`), optional rubric
|
|
18
|
+
- \`timeLimit\` (seconds or minutes depending on screen — e.g. timer UI may use “minutes” while the field is \`timeLimit\`)
|
|
19
|
+
- \`attempts\`: max attempts
|
|
20
|
+
- \`feedback\`: \`onCorrect\`, \`onIncorrect\`, \`onPartial\`, \`showExplanation\`, \`explanation\`
|
|
21
|
+
|
|
22
|
+
**Accessibility / adaptive replacement** (text, PDF, presentation, video, browser) in the side panel:
|
|
23
|
+
- \`settings.accessibilityMode\`: \`auto\` | \`assist\` | \`manual\`
|
|
24
|
+
- \`settings.forceAccessibleGame\`: boolean
|
|
25
|
+
|
|
26
|
+
**API persistence**: blocks are saved on the EduFlow document via \`/api/eduflows/:id\` (JSON update).
|
|
27
|
+
`;
|
|
28
|
+
export const EDUFLOW_BLOCK_DOCS = [
|
|
29
|
+
{
|
|
30
|
+
id: 'start',
|
|
31
|
+
category: 'Structure',
|
|
32
|
+
label: 'Start',
|
|
33
|
+
summary: 'Entry point of the EduFlow path.',
|
|
34
|
+
configurationMd: `### Role
|
|
35
|
+
Marks the beginning of the flow; often unique and institution-locked.
|
|
36
|
+
|
|
37
|
+
### Specifics
|
|
38
|
+
No required \`settings\`. Use title/description as the designer cue.
|
|
39
|
+
|
|
40
|
+
### Graph
|
|
41
|
+
One or more outputs to the following blocks.`,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'end',
|
|
45
|
+
category: 'Structure',
|
|
46
|
+
label: 'End',
|
|
47
|
+
summary: 'Path completion / exit.',
|
|
48
|
+
configurationMd: `### Role
|
|
49
|
+
Ends the path (certificate, wrap-up, exit).
|
|
50
|
+
|
|
51
|
+
### Specifics
|
|
52
|
+
No mandatory technical fields in the generic designer.
|
|
53
|
+
|
|
54
|
+
### Graph
|
|
55
|
+
Input(s) from the main path or branches.`,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'subflow',
|
|
59
|
+
category: 'Structure',
|
|
60
|
+
label: 'Subflow',
|
|
61
|
+
summary: 'Reference or embed another EduFlow.',
|
|
62
|
+
configurationMd: `### Role
|
|
63
|
+
Chains a submodule (another course or reusable segment).
|
|
64
|
+
|
|
65
|
+
### Specifics
|
|
66
|
+
Dedicated \`SubflowBlockEditor\`: link to an existing EduFlow (id, embed options). Exact \`settings\` fields depend on the editor (target flow selection, etc.).`,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'loop',
|
|
70
|
+
category: 'Structure',
|
|
71
|
+
label: 'Loop',
|
|
72
|
+
summary: 'Repeat a segment based on a condition.',
|
|
73
|
+
configurationMd: `### settings
|
|
74
|
+
- \`loopCondition\`: \`count\` | \`score\` | \`completion\`
|
|
75
|
+
- \`maxIterations\`: integer (1–100, default 3)
|
|
76
|
+
|
|
77
|
+
### Role
|
|
78
|
+
Revisit a set of blocks until a criterion is met.`,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'timer',
|
|
82
|
+
category: 'Structure',
|
|
83
|
+
label: 'Timer',
|
|
84
|
+
summary: 'Time limit for a step.',
|
|
85
|
+
configurationMd: `### data.timeLimit
|
|
86
|
+
Shown in **minutes** in the designer UI (integer ≥ 1).
|
|
87
|
+
|
|
88
|
+
> Note: the side panel may use a \`duration\` field on other screens; the main canvas uses \`timeLimit\`.`,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 'summary',
|
|
92
|
+
category: 'Structure',
|
|
93
|
+
label: 'Summary',
|
|
94
|
+
summary: 'Recap for the learner.',
|
|
95
|
+
configurationMd: `### Role
|
|
96
|
+
Consolidation block (structured text, key points).
|
|
97
|
+
|
|
98
|
+
### Specifics
|
|
99
|
+
\`SummaryBlockEditor\`: rich content and internal options in \`settings\` / \`content\` per editor.`,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: 'form',
|
|
103
|
+
category: 'Structure',
|
|
104
|
+
label: 'Form',
|
|
105
|
+
summary: 'Structured data collection (dynamic fields).',
|
|
106
|
+
configurationMd: `### settings.formFields
|
|
107
|
+
Array of field objects: \`id\`, \`type\` (\`text\` | \`textarea\` | \`email\` | \`number\` | \`select\` | \`checkbox\` | \`radio\` | \`date\` | \`toggle\` | \`subtitle\` | \`file\`), \`label\`, \`placeholder\`, \`required\`, \`options\` (for select/radio), \`defaultValue\`, \`min\` / \`max\`, \`maxLength\`, \`description\`, \`autoFill\` (\`email\` | \`firstName\` | \`lastName\` | \`fullName\`).
|
|
108
|
+
|
|
109
|
+
### Conditional rules
|
|
110
|
+
\`settings.formRules\`: \`if\` / \`elseif\` / \`else\` rules with conditions on fields.
|
|
111
|
+
|
|
112
|
+
### data
|
|
113
|
+
Title, description; optional description audio if enabled on the flow.`,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: 'split',
|
|
117
|
+
category: 'Structure',
|
|
118
|
+
label: 'Split (branch)',
|
|
119
|
+
summary: 'Multiple optional paths.',
|
|
120
|
+
configurationMd: `### settings
|
|
121
|
+
- \`splitTitle\`: label shown (e.g. “Choose your path”)
|
|
122
|
+
- \`numOutputs\`: number of branches (2–10)
|
|
123
|
+
|
|
124
|
+
### Graph
|
|
125
|
+
As many outputs as \`numOutputs\`.`,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: 'merge',
|
|
129
|
+
category: 'Structure',
|
|
130
|
+
label: 'Merge',
|
|
131
|
+
summary: 'Join multiple branches.',
|
|
132
|
+
configurationMd: `### Role
|
|
133
|
+
Convergence after a \`split\` or parallel paths.
|
|
134
|
+
|
|
135
|
+
### Specifics
|
|
136
|
+
No dedicated fields in the generic UI; title/description for the designer.`,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: 'email',
|
|
140
|
+
category: 'Structure',
|
|
141
|
+
label: 'Email',
|
|
142
|
+
summary: 'Send email (template) in the flow.',
|
|
143
|
+
configurationMd: `### settings
|
|
144
|
+
- \`emailSubject\`: subject line
|
|
145
|
+
- \`emailTemplate\`: HTML body (placeholders e.g. \`{{name}}\`)`,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: 'text',
|
|
149
|
+
category: 'Content',
|
|
150
|
+
label: 'Text',
|
|
151
|
+
summary: 'Rich content: article, instructions.',
|
|
152
|
+
configurationMd: `### data.content
|
|
153
|
+
Body HTML (rich editor).
|
|
154
|
+
|
|
155
|
+
### settings
|
|
156
|
+
Typography / layout per \`TextBlockEditor\` (fonts, line height, etc. when exposed).`,
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: 'video',
|
|
160
|
+
category: 'Content',
|
|
161
|
+
label: 'Video',
|
|
162
|
+
summary: 'Video player (upload, YouTube, Vimeo, URL).',
|
|
163
|
+
configurationMd: `### Source
|
|
164
|
+
\`settings\` control source type (\`upload\` | \`youtube\` | \`vimeo\` | \`url\`), IDs or URL, file metadata.
|
|
165
|
+
|
|
166
|
+
### Appearance
|
|
167
|
+
Player theme: \`app\` | \`cinema\` | \`glass\` | \`editorial\`; accent color; ratio \`16:9\` | \`21:9\` | \`4:3\` | \`1:1\`; corners \`soft\` | \`rounded\` | \`square\`; shadow \`soft\` | \`glow\` | \`dramatic\`.
|
|
168
|
+
|
|
169
|
+
### Subtitles
|
|
170
|
+
Tracks per language (fr, en, es, de, it) per editor.
|
|
171
|
+
|
|
172
|
+
See \`apps/frontend/src/lib/video-block.ts\` for exported keys (\`getVideoBlockConfig\`).`,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: 'podcast',
|
|
176
|
+
category: 'Content',
|
|
177
|
+
label: 'Podcast',
|
|
178
|
+
summary: 'Long-form podcast audio (episode / feed).',
|
|
179
|
+
configurationMd: `### Role
|
|
180
|
+
Long audio playback linked to the podcasts module when configured.
|
|
181
|
+
|
|
182
|
+
### data / settings
|
|
183
|
+
Media URL, episode metadata in \`settings\` per block editor (series title, episode, artwork).`,
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: 'audio',
|
|
187
|
+
category: 'Content',
|
|
188
|
+
label: 'Audio',
|
|
189
|
+
summary: 'Audio track (outside podcast module).',
|
|
190
|
+
configurationMd: `### Role
|
|
191
|
+
Simple audio playback (uploaded file or URL).
|
|
192
|
+
|
|
193
|
+
### data / settings
|
|
194
|
+
\`mediaUrl\` or upload; \`AudioBlockEditor\`: default volume, loop, waveform, subtitles/transcript when available.`,
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
id: 'image',
|
|
198
|
+
category: 'Content',
|
|
199
|
+
label: 'Image',
|
|
200
|
+
summary: 'Static or responsive image.',
|
|
201
|
+
configurationMd: `### data.mediaUrl or upload
|
|
202
|
+
Image file; \`settings\`: caption, alt, size, clickable link per \`ImageBlockEditor\`.`,
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: 'pdf',
|
|
206
|
+
category: 'Content',
|
|
207
|
+
label: 'PDF',
|
|
208
|
+
summary: 'Embedded PDF document.',
|
|
209
|
+
configurationMd: `### Role
|
|
210
|
+
Display / download document.
|
|
211
|
+
|
|
212
|
+
### data / settings
|
|
213
|
+
PDF URL, toolbar options, default page, embed mode (per \`PDFBlockEditor\`).`,
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: 'presentation',
|
|
217
|
+
category: 'Content',
|
|
218
|
+
label: 'Presentation',
|
|
219
|
+
summary: 'Slide deck.',
|
|
220
|
+
configurationMd: `### settings
|
|
221
|
+
Slides: array of objects (title, content, media) managed by \`PresentationBlockEditor\`.`,
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
id: 'browser',
|
|
225
|
+
category: 'Content',
|
|
226
|
+
label: 'Embedded browser',
|
|
227
|
+
summary: 'iframe / webview to an external URL.',
|
|
228
|
+
configurationMd: `### settings
|
|
229
|
+
Allowed URL, sandbox flags, height — via \`BrowserBlockEditor\`.`,
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
id: 'chart',
|
|
233
|
+
category: 'Content',
|
|
234
|
+
label: 'Chart',
|
|
235
|
+
summary: 'Chart (bars, line, etc.).',
|
|
236
|
+
configurationMd: `### settings
|
|
237
|
+
Dataset (series, labels), chart type, colors — \`ChartBlockEditor\`.`,
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
id: 'timeline',
|
|
241
|
+
category: 'Content',
|
|
242
|
+
label: 'Timeline',
|
|
243
|
+
summary: 'Dated events on a line.',
|
|
244
|
+
configurationMd: `### settings
|
|
245
|
+
Event list: date, title, description, media — \`TimelineBlockEditor\`.`,
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
id: 'mindmap',
|
|
249
|
+
category: 'Content',
|
|
250
|
+
label: 'Mind map',
|
|
251
|
+
summary: 'Hierarchical nodes and links.',
|
|
252
|
+
configurationMd: `### settings
|
|
253
|
+
Tree structure (root node, children, styles) — \`MindmapBlockEditor\`.`,
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
id: 'visual_ai',
|
|
257
|
+
category: 'Content',
|
|
258
|
+
label: 'AI visual',
|
|
259
|
+
summary: 'AI-generated image or media.',
|
|
260
|
+
configurationMd: `### settings
|
|
261
|
+
Prompt, style, provider, resulting image (URL or media id) — \`VisualAiBlockEditor\` / related modals.`,
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
id: 'quiz_mcq',
|
|
265
|
+
category: 'Interactive',
|
|
266
|
+
label: 'MCQ / quiz',
|
|
267
|
+
summary: 'Multiple choice, true/false, or ordering.',
|
|
268
|
+
configurationMd: `### Side panel (\`QuizBlockEditor\`)
|
|
269
|
+
- \`settings.quizType\`: \`mcq\` | \`true_false\` | \`ordering\`
|
|
270
|
+
- \`data.content\`: question stem (HTML/text)
|
|
271
|
+
- \`settings.answers\`: array \`{ id, text, isCorrect }\` (MCQ, max 10 answers)
|
|
272
|
+
- \`settings.orderingItems\`: array \`{ id, text, order }\` (ordering mode)
|
|
273
|
+
- \`settings.shuffleAnswers\`: shuffle options
|
|
274
|
+
- \`settings.correctAnswer\`: boolean (T/F mode)
|
|
275
|
+
- \`settings.pointsPerCorrect\`, \`settings.partialScoring\`
|
|
276
|
+
- \`data.attempts\`: max attempts
|
|
277
|
+
- \`data.feedback\`: correct/incorrect messages, \`showExplanation\`, \`explanation\`
|
|
278
|
+
|
|
279
|
+
### Canvas panel (extended MCQ)
|
|
280
|
+
- \`settings.optionA\` … \`optionD\`: answer texts
|
|
281
|
+
- \`settings.correctAnswer\`: \`A\` | \`B\` | \`C\` | \`D\`
|
|
282
|
+
- \`settings.branchingMode\`: \`none\` | \`correct_incorrect\` | \`per_answer\` (graph branching)
|
|
283
|
+
- \`settings.feedback\`: message after answer`,
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
id: 'quiz_open',
|
|
287
|
+
category: 'Interactive',
|
|
288
|
+
label: 'Open question',
|
|
289
|
+
summary: 'Free-text answer.',
|
|
290
|
+
configurationMd: `### Role
|
|
291
|
+
Learner enters a long answer; manual or AI grading per product settings.
|
|
292
|
+
|
|
293
|
+
### data
|
|
294
|
+
\`content\`: prompt. \`settings\`: criteria, max length, rubric when present in UI.`,
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
id: 'phrase_reveal',
|
|
298
|
+
category: 'Interactive',
|
|
299
|
+
label: 'Phrase reveal',
|
|
300
|
+
summary: 'Words or chunks revealed progressively.',
|
|
301
|
+
configurationMd: `### settings / content
|
|
302
|
+
Base text, splitting, animation — \`PhraseRevealBlockEditor\`.`,
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
id: 'self_assessment',
|
|
306
|
+
category: 'Interactive',
|
|
307
|
+
label: 'Self-assessment',
|
|
308
|
+
summary: 'Self-rated criteria grid.',
|
|
309
|
+
configurationMd: `### settings.criteria
|
|
310
|
+
Array \`EduFlowSelfAssessmentCriterion\`: \`id\`, \`label\`, \`description\`, \`badgeOptions\` (if empty → free text per criterion).
|
|
311
|
+
|
|
312
|
+
### Global response type
|
|
313
|
+
\`settings.responseType\`: \`numeric\` | \`stars\` | \`text\` (see \`eduflow.ts\`).`,
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
id: 'voice_assessment',
|
|
317
|
+
category: 'Interactive',
|
|
318
|
+
label: 'Voice assessment',
|
|
319
|
+
summary: 'Pronunciation / recitation vs a reference.',
|
|
320
|
+
configurationMd: `### settings
|
|
321
|
+
- \`referenceText\`: text to recite
|
|
322
|
+
- \`minScore\`: minimum score (0–100%, default 70)`,
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
id: 'evaluation',
|
|
326
|
+
category: 'Interactive',
|
|
327
|
+
label: 'Evaluation / grading',
|
|
328
|
+
summary: 'Structured grading rubric.',
|
|
329
|
+
configurationMd: `### data.scoring
|
|
330
|
+
\`maxPoints\`, \`passingScore\`, \`scoringMethod\`, \`rubric\` (criteria, levels, points).
|
|
331
|
+
|
|
332
|
+
### settings
|
|
333
|
+
Extra fields in \`EvaluationBlockEditor\` (scales, competencies).`,
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
id: 'poll',
|
|
337
|
+
category: 'Interactive',
|
|
338
|
+
label: 'Poll',
|
|
339
|
+
summary: 'Ungraded vote or opinion.',
|
|
340
|
+
configurationMd: `### settings
|
|
341
|
+
- \`pollQuestion\`: question
|
|
342
|
+
- \`pollOptions\`: multiline text, **one option per line**
|
|
343
|
+
- \`showLiveResults\`: live results`,
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
id: 'dys_reader',
|
|
347
|
+
category: 'Accessibility',
|
|
348
|
+
label: 'DYS reader',
|
|
349
|
+
summary: 'Text with reading aids (syllables, TTS).',
|
|
350
|
+
configurationMd: `### settings
|
|
351
|
+
- \`dysText\`: HTML shown
|
|
352
|
+
- \`ttsEnabled\`: auto audio playback`,
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
id: 'dys_reading_practice',
|
|
356
|
+
category: 'Accessibility',
|
|
357
|
+
label: 'DYS reading practice',
|
|
358
|
+
summary: 'Guided reading exercise.',
|
|
359
|
+
configurationMd: `### settings
|
|
360
|
+
Same as dys_reader: \`dysText\`, \`ttsEnabled\`.`,
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
id: 'dys_image_zones',
|
|
364
|
+
category: 'Accessibility',
|
|
365
|
+
label: 'DYS image zones',
|
|
366
|
+
summary: 'Clickable zones on an image for reading support.',
|
|
367
|
+
configurationMd: `### settings
|
|
368
|
+
Zone definitions (coordinates, labels, audio) per DYS UI; often JSON in \`settings\`.`,
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
id: 'dys_clock',
|
|
372
|
+
category: 'Accessibility',
|
|
373
|
+
label: 'DYS teaching clock',
|
|
374
|
+
summary: 'Adapted time representation.',
|
|
375
|
+
configurationMd: `### settings
|
|
376
|
+
Display format, speed, visual aids (see DYS editor).`,
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
id: 'glossary',
|
|
380
|
+
category: 'Accessibility',
|
|
381
|
+
label: 'Glossary',
|
|
382
|
+
summary: 'Terms and definitions in the path.',
|
|
383
|
+
configurationMd: `### settings.termsJson
|
|
384
|
+
JSON string: array \`{ term, definition, quiz?: boolean }\`.`,
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
id: 'html',
|
|
388
|
+
category: 'Enterprise',
|
|
389
|
+
label: 'Embedded HTML / JS',
|
|
390
|
+
summary: 'Controlled HTML or script snippet.',
|
|
391
|
+
configurationMd: `### settings.htmlCode
|
|
392
|
+
HTML (and possibly inline JS per security policy). Use with care (XSS).`,
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
id: 'certification',
|
|
396
|
+
category: 'Enterprise',
|
|
397
|
+
label: 'Certification (document)',
|
|
398
|
+
summary: 'Generate a certificate from a Word template.',
|
|
399
|
+
configurationMd: `### settings
|
|
400
|
+
- \`templateUrl\`: URL of the .docx template
|
|
401
|
+
- \`variablesJson\`: JSON mapping placeholders → learner fields (\`"{{NAME}}": "learner.lastName"\`, etc.)`,
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
id: 'signature',
|
|
405
|
+
category: 'Enterprise',
|
|
406
|
+
label: 'Signature',
|
|
407
|
+
summary: 'Handwritten or electronic signature.',
|
|
408
|
+
configurationMd: `### settings
|
|
409
|
+
Signature canvas config, legal wording, consent text — per block editor.`,
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
id: 'facial_recognition',
|
|
413
|
+
category: 'Enterprise',
|
|
414
|
+
label: 'Facial recognition',
|
|
415
|
+
summary: 'Face-based identity check.',
|
|
416
|
+
configurationMd: `### settings
|
|
417
|
+
Confidence threshold, GDPR consent, error messages — per implementation and AWS Rekognition on the backend.`,
|
|
418
|
+
},
|
|
419
|
+
];
|
|
420
|
+
const byId = new Map(EDUFLOW_BLOCK_DOCS.map((e) => [e.id, e]));
|
|
421
|
+
export function renderEduflowBlockMarkdown(typeId) {
|
|
422
|
+
const e = byId.get(typeId);
|
|
423
|
+
if (!e) {
|
|
424
|
+
return `# Unknown block (\`${typeId}\`)\n\nNo dedicated sheet. See \`lyrra_eduflow_blocks_index\` and \`BlockType\` in the frontend.\n\n${EDUFLOW_BLOCK_COMMON}`;
|
|
425
|
+
}
|
|
426
|
+
return `# ${e.label} — type \`${e.id}\`
|
|
427
|
+
|
|
428
|
+
**Category:** ${e.category}
|
|
429
|
+
|
|
430
|
+
${e.configurationMd}
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
${EDUFLOW_BLOCK_COMMON}`;
|
|
435
|
+
}
|
|
436
|
+
export function eduflowBlockToolName(typeId) {
|
|
437
|
+
return `lyrra_eduflow_block_${typeId.replace(/[^a-zA-Z0-9_]/g, '_')}`;
|
|
438
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
function single(h) {
|
|
2
|
+
if (h === undefined)
|
|
3
|
+
return undefined;
|
|
4
|
+
return Array.isArray(h) ? h[0] : h;
|
|
5
|
+
}
|
|
6
|
+
const HOP_BY_HOP = new Set([
|
|
7
|
+
'connection',
|
|
8
|
+
'keep-alive',
|
|
9
|
+
'proxy-authenticate',
|
|
10
|
+
'proxy-authorization',
|
|
11
|
+
'te',
|
|
12
|
+
'trailers',
|
|
13
|
+
'transfer-encoding',
|
|
14
|
+
'upgrade',
|
|
15
|
+
'host',
|
|
16
|
+
'content-length',
|
|
17
|
+
]);
|
|
18
|
+
/**
|
|
19
|
+
* En-têtes d’auth reçus sur le MCP HTTP à réutiliser vers l’API Lyrra.
|
|
20
|
+
* n8n « Header Auth » envoie en général X-Lyrra-Api-Key (ou Authorization).
|
|
21
|
+
*/
|
|
22
|
+
export function pickIncomingAuthHeaders(headers) {
|
|
23
|
+
const out = {};
|
|
24
|
+
const auth = single(headers['authorization']);
|
|
25
|
+
if (auth)
|
|
26
|
+
out.Authorization = auth;
|
|
27
|
+
const lyrra = single(headers['x-lyrra-api-key']);
|
|
28
|
+
if (lyrra)
|
|
29
|
+
out['X-Lyrra-Api-Key'] = lyrra;
|
|
30
|
+
const xak = single(headers['x-api-key']);
|
|
31
|
+
if (xak)
|
|
32
|
+
out['X-Api-Key'] = xak;
|
|
33
|
+
const extra = process.env.LYRRA_MCP_EXTRA_INBOUND_HEADERS?.split(',') ?? [];
|
|
34
|
+
for (const raw of extra) {
|
|
35
|
+
const display = raw.trim();
|
|
36
|
+
if (!display)
|
|
37
|
+
continue;
|
|
38
|
+
const lower = display.toLowerCase();
|
|
39
|
+
if (HOP_BY_HOP.has(lower))
|
|
40
|
+
continue;
|
|
41
|
+
const val = single(headers[lower]);
|
|
42
|
+
if (val)
|
|
43
|
+
out[display] = val;
|
|
44
|
+
}
|
|
45
|
+
if (Object.keys(out).length === 0)
|
|
46
|
+
return null;
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MCP Streamable HTTP — pour n8n / clients qui attendent une URL https://…/mcp
|
|
4
|
+
* (Header Auth : X-Lyrra-Api-Key ou Authorization, aligné tableau de bord institution).
|
|
5
|
+
*/
|
|
6
|
+
import express from 'express';
|
|
7
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
8
|
+
import { createLyrraMcpServer } from './lyrra-mcp-core.js';
|
|
9
|
+
import { runWithInboundAuthHeaders, requestOrigin } from './auth-session.js';
|
|
10
|
+
import { pickIncomingAuthHeaders } from './http-incoming-auth.js';
|
|
11
|
+
const PORT = parseInt(process.env.LYRRA_MCP_HTTP_PORT ?? '3457', 10);
|
|
12
|
+
let mcpSingleton = null;
|
|
13
|
+
async function getMcp() {
|
|
14
|
+
mcpSingleton ??= createLyrraMcpServer().then((r) => r.mcp);
|
|
15
|
+
return mcpSingleton;
|
|
16
|
+
}
|
|
17
|
+
/** Sérialise les requêtes MCP : un seul transport à la fois sur l’instance McpServer. */
|
|
18
|
+
let mutexChain = Promise.resolve();
|
|
19
|
+
function enqueueMcp(fn) {
|
|
20
|
+
const next = mutexChain.then(() => fn());
|
|
21
|
+
mutexChain = next.then(() => undefined, () => undefined);
|
|
22
|
+
return next;
|
|
23
|
+
}
|
|
24
|
+
async function validateAuth(headers) {
|
|
25
|
+
const origin = requestOrigin();
|
|
26
|
+
const url = `${origin}/api/auth/me`;
|
|
27
|
+
const r = await fetch(url, {
|
|
28
|
+
headers: { Accept: 'application/json', ...headers },
|
|
29
|
+
});
|
|
30
|
+
return r.ok;
|
|
31
|
+
}
|
|
32
|
+
const app = express();
|
|
33
|
+
app.disable('x-powered-by');
|
|
34
|
+
app.use('/mcp', express.json({ limit: '50mb' }));
|
|
35
|
+
app.all('/mcp', async (req, res) => {
|
|
36
|
+
const auth = pickIncomingAuthHeaders(req.headers);
|
|
37
|
+
if (!auth) {
|
|
38
|
+
res.status(401).json({
|
|
39
|
+
success: false,
|
|
40
|
+
error: 'Missing auth: use n8n Header Auth with X-Lyrra-Api-Key (institution key) or Authorization Bearer.',
|
|
41
|
+
});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const skipValidate = process.env.LYRRA_MCP_SKIP_AUTH_VALIDATE === '1';
|
|
45
|
+
if (!skipValidate) {
|
|
46
|
+
const ok = await validateAuth(auth);
|
|
47
|
+
if (!ok) {
|
|
48
|
+
res.status(401).json({
|
|
49
|
+
success: false,
|
|
50
|
+
error: 'Invalid Lyrra credentials (GET /api/auth/me failed). Check your Header Auth value.',
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
await runWithInboundAuthHeaders(auth, async () => enqueueMcp(async () => {
|
|
57
|
+
const mcp = await getMcp();
|
|
58
|
+
const transport = new StreamableHTTPServerTransport({
|
|
59
|
+
sessionIdGenerator: undefined,
|
|
60
|
+
});
|
|
61
|
+
await mcp.connect(transport);
|
|
62
|
+
try {
|
|
63
|
+
await transport.handleRequest(req, res, req.body);
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
try {
|
|
67
|
+
await transport.close();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* ignore */
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
await mcp.close();
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
/* ignore */
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
process.stderr.write(`[lyrra-mcp-http] ${e instanceof Error ? e.stack ?? e.message : e}\n`);
|
|
83
|
+
if (!res.headersSent) {
|
|
84
|
+
res.status(500).json({
|
|
85
|
+
jsonrpc: '2.0',
|
|
86
|
+
error: { code: -32603, message: 'Internal server error' },
|
|
87
|
+
id: null,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
app.get('/health', (_req, res) => {
|
|
93
|
+
res.status(200).type('text/plain').send('ok');
|
|
94
|
+
});
|
|
95
|
+
async function main() {
|
|
96
|
+
await getMcp();
|
|
97
|
+
app.listen(PORT, '0.0.0.0', () => {
|
|
98
|
+
process.stderr.write(`[lyrra-mcp-http] listening 0.0.0.0:${PORT} path /mcp health /health\n`);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
main().catch((e) => {
|
|
102
|
+
process.stderr.write(`[lyrra-mcp-http] Fatal: ${e instanceof Error ? e.stack ?? e.message : e}\n`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Lyrra Studio MCP stdio server.
|
|
4
|
+
* Run: `npm run build && npm start` in apps/mcp-server (or `npm run dev`).
|
|
5
|
+
* HTTP (n8n URL) : `npm run start:http` → voir http-main.ts
|
|
6
|
+
*/
|
|
2
7
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
// --- Start server (stdio mode) ---
|
|
8
|
+
import { getLyrraRequestAuthHeaders } from './auth-session.js';
|
|
9
|
+
import { createLyrraMcpServer } from './lyrra-mcp-core.js';
|
|
6
10
|
async function main() {
|
|
7
|
-
|
|
8
|
-
|
|
11
|
+
const { mcp } = await createLyrraMcpServer();
|
|
12
|
+
try {
|
|
13
|
+
await getLyrraRequestAuthHeaders();
|
|
14
|
+
process.stderr.write('[lyrra-mcp] API credentials OK.\n');
|
|
9
15
|
}
|
|
10
|
-
|
|
11
|
-
|
|
16
|
+
catch (e) {
|
|
17
|
+
process.stderr.write(`[lyrra-mcp] Auth warning: ${e instanceof Error ? e.message : e}\n`);
|
|
12
18
|
}
|
|
13
|
-
const server = createMcpServer();
|
|
14
19
|
const transport = new StdioServerTransport();
|
|
15
|
-
await
|
|
20
|
+
await mcp.connect(transport);
|
|
16
21
|
}
|
|
17
|
-
main().catch((
|
|
18
|
-
|
|
22
|
+
main().catch((e) => {
|
|
23
|
+
process.stderr.write(`[lyrra-mcp] Fatal: ${e instanceof Error ? e.stack ?? e.message : e}\n`);
|
|
19
24
|
process.exit(1);
|
|
20
25
|
});
|
|
21
|
-
//# sourceMappingURL=index.js.map
|