@thlg057/mo5-rag-mcp 1.0.0

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/index.js ADDED
@@ -0,0 +1,592 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import {
6
+ ListToolsRequestSchema,
7
+ CallToolRequestSchema,
8
+ ListResourcesRequestSchema,
9
+ ReadResourceRequestSchema,
10
+ ListPromptsRequestSchema,
11
+ GetPromptRequestSchema
12
+ } from "@modelcontextprotocol/sdk/types.js";
13
+ import { spawn } from "child_process";
14
+ import { fileURLToPath } from "url";
15
+ import path from "path";
16
+
17
+ // Récupération de l'URL du NAS avec fallback
18
+ const RAG_BASE_URL = process.env.RAG_BASE_URL ?? "http://nas:8080";
19
+
20
+ // Répertoire des scripts Python (relatif à index.js)
21
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
22
+ const SCRIPTS_DIR = path.join(__dirname, "scripts");
23
+
24
+ /**
25
+ * Exécute un script Python et retourne { stdout, stderr }
26
+ * Rejette avec l'erreur complète si le code de sortie est non-zéro.
27
+ */
28
+ function runScript(scriptName, args) {
29
+ return new Promise((resolve, reject) => {
30
+ const scriptPath = path.join(SCRIPTS_DIR, scriptName);
31
+ const child = spawn("python3", [scriptPath, ...args]);
32
+
33
+ let stdout = "";
34
+ let stderr = "";
35
+
36
+ child.stdout.on("data", (d) => { stdout += d; });
37
+ child.stderr.on("data", (d) => { stderr += d; });
38
+
39
+ child.on("close", (code) => {
40
+ if (code === 0) {
41
+ resolve({ stdout, stderr });
42
+ } else {
43
+ reject(new Error(`Exit ${code}\n${stderr || stdout}`));
44
+ }
45
+ });
46
+
47
+ child.on("error", (err) => {
48
+ reject(new Error(`Impossible de lancer python3 : ${err.message}`));
49
+ });
50
+ });
51
+ }
52
+
53
+ /**
54
+ * Utilitaire fetch avec Timeout et Retry
55
+ */
56
+ async function fetchWithRetry(url, options = {}, retries = 3, backoff = 1000) {
57
+ const { timeout = 30000, ...fetchOptions } = options;
58
+
59
+ for (let i = 0; i < retries; i++) {
60
+ const controller = new AbortController();
61
+ const id = setTimeout(() => controller.abort(), timeout);
62
+
63
+ try {
64
+ const response = await fetch(url, {
65
+ ...fetchOptions,
66
+ signal: controller.signal
67
+ });
68
+
69
+ clearTimeout(id);
70
+ return response;
71
+ } catch (err) {
72
+ clearTimeout(id);
73
+ const isLastAttempt = i === retries - 1;
74
+
75
+ if (isLastAttempt) throw err;
76
+
77
+ const delay = backoff * Math.pow(2, i);
78
+ console.error(`Tentative ${i + 1} échouée, nouvel essai dans ${delay}ms...`);
79
+ await new Promise(resolve => setTimeout(resolve, delay));
80
+ }
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Recherche sémantique avec fallback automatique sur le score de similarité.
86
+ * Si aucun résultat à `minScore`, on retente à `fallbackScore`.
87
+ */
88
+ async function semanticSearch({ query, tags, maxResults = 5, minScore = 0.7, fallbackScore = 0.5 }) {
89
+ const doSearch = async (score) => {
90
+ const res = await fetchWithRetry(`${RAG_BASE_URL}/api/Search`, {
91
+ method: "POST",
92
+ headers: { "Content-Type": "application/json" },
93
+ body: JSON.stringify({
94
+ query,
95
+ tags,
96
+ maxResults,
97
+ minSimilarityScore: score,
98
+ includeMetadata: true,
99
+ }),
100
+ timeout: 30000
101
+ }, 3);
102
+
103
+ if (!res.ok) {
104
+ const errorText = await res.text();
105
+ throw new Error(`Erreur API NAS (${res.status}): ${errorText}`);
106
+ }
107
+
108
+ return res.json();
109
+ };
110
+
111
+ console.error(`DEBUG semantic_search: query="${query}", minScore=${minScore}`);
112
+
113
+ let data = await doSearch(minScore);
114
+
115
+ // Fallback si aucun résultat avec le score initial
116
+ if ((!data.results || data.results.length === 0) && fallbackScore < minScore) {
117
+ console.error(`Aucun résultat à ${minScore}, fallback à ${fallbackScore}...`);
118
+ data = await doSearch(fallbackScore);
119
+ data._usedFallback = true;
120
+ data._fallbackScore = fallbackScore;
121
+ }
122
+
123
+ return data;
124
+ }
125
+
126
+ /**
127
+ * Initialisation du serveur MCP
128
+ */
129
+ const server = new Server(
130
+ {
131
+ name: "mo5-rag-mcp",
132
+ version: "1.0.0",
133
+ },
134
+ {
135
+ capabilities: {
136
+ tools: {},
137
+ resources: {},
138
+ prompts: {},
139
+ },
140
+ }
141
+ );
142
+
143
+ /* ============================================================
144
+ TOOLS (Outils actionnables par l'IA)
145
+ ============================================================ */
146
+
147
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
148
+ return {
149
+ tools: [
150
+ // --- Outils RAG ---
151
+ {
152
+ name: "semantic_search",
153
+ description: "Recherche sémantique dans la documentation MO5 (base de connaissances). Si aucun résultat n'est trouvé avec le score demandé, une seconde tentative est effectuée automatiquement avec un seuil plus permissif.",
154
+ inputSchema: {
155
+ type: "object",
156
+ properties: {
157
+ query: { type: "string", description: "La question ou les mots-clés de recherche" },
158
+ tags: { type: "array", items: { type: "string" }, description: "Filtres par thématiques" },
159
+ maxResults: { type: "number", default: 5 },
160
+ minSimilarityScore: { type: "number", default: 0.7, description: "Score minimum de similarité (0.0 à 1.0). Un fallback automatique à 0.5 est appliqué si aucun résultat n'est trouvé." },
161
+ },
162
+ required: ["query"],
163
+ },
164
+ },
165
+ {
166
+ name: "get_chunk_context",
167
+ description: "Récupère le contexte élargi autour d'un chunk retourné par semantic_search : les chunks voisins (avant/après) dans le même document. Utile quand une explication technique est découpée sur plusieurs chunks et que la réponse semble incomplète ou tronquée.",
168
+ inputSchema: {
169
+ type: "object",
170
+ properties: {
171
+ documentId: { type: "string", description: "L'ID du document (champ document.documentId dans les résultats de semantic_search)" },
172
+ chunkIndex: { type: "number", description: "L'index du chunk central (champ position.chunkIndex dans les résultats de semantic_search)" },
173
+ contextSize: { type: "number", default: 2, description: "Nombre de chunks à récupérer de chaque côté (défaut: 2)" },
174
+ },
175
+ required: ["documentId", "chunkIndex"],
176
+ },
177
+ },
178
+ {
179
+ name: "list_official_docs",
180
+ description: "Liste les documents officiels et de développement Thomson MO5 (manuels, guides techniques). Utile pour identifier quelles ressources existent avant de faire une recherche sémantique ciblée, ou pour fournir un lien de téléchargement direct à l'utilisateur.",
181
+ inputSchema: {
182
+ type: "object",
183
+ properties: {
184
+ tag: { type: "string", description: "Filtrer par tag (ex: 'basic', 'assembleur', 'hardware'). Optionnel." },
185
+ },
186
+ required: [],
187
+ },
188
+ },
189
+
190
+ // --- Outils de build MO5 ---
191
+ {
192
+ name: "make_fd",
193
+ description: "Génère une image disquette .fd bootable pour Thomson MO5 à partir d'un ou plusieurs binaires compilés avec CMOC. Le boot loader MO5 est embarqué directement dans le script — aucune dépendance externe requise. Équivalent de : fdfs -addBL output.fd BOOTMO.BIN program.BIN",
194
+ inputSchema: {
195
+ type: "object",
196
+ properties: {
197
+ output_fd: {
198
+ type: "string",
199
+ description: "Chemin de sortie de l'image disquette, ex: output/MYAPP.fd"
200
+ },
201
+ input_bins: {
202
+ type: "array",
203
+ items: { type: "string" },
204
+ description: "Liste des chemins vers les fichiers .BIN compilés avec CMOC (avec header Thomson)"
205
+ }
206
+ },
207
+ required: ["output_fd", "input_bins"]
208
+ }
209
+ },
210
+ {
211
+ name: "fd_to_sd",
212
+ description: "Convertit une image disquette .fd (format 3.5\" 720 Ko) en .sd (format 5.25\" 320 Ko) pour une utilisation avec SDDrive — le composant qui simule un lecteur de disquette sur Thomson MO5 via carte SD.",
213
+ inputSchema: {
214
+ type: "object",
215
+ properties: {
216
+ input_fd: {
217
+ type: "string",
218
+ description: "Chemin vers le fichier .fd source, ex: output/MYAPP.fd"
219
+ },
220
+ output_sd: {
221
+ type: "string",
222
+ description: "Chemin de sortie du fichier .sd, ex: output/MYAPP.sd"
223
+ }
224
+ },
225
+ required: ["input_fd", "output_sd"]
226
+ }
227
+ },
228
+ {
229
+ name: "png_to_mo5_sprite",
230
+ description: "Convertit une image PNG en tableaux C (FORME + COULEUR) pour le Thomson MO5. Génère un fichier .h directement utilisable avec le SDK MO5 (mo5_sprite.h). Format : 1 octet = 8 pixels, 2 couleurs par groupe de 8 pixels.",
231
+ inputSchema: {
232
+ type: "object",
233
+ properties: {
234
+ image_path: {
235
+ type: "string",
236
+ description: "Chemin vers le fichier PNG source, ex: assets/hero.png"
237
+ },
238
+ output_path: {
239
+ type: "string",
240
+ description: "Chemin de sortie du fichier .h généré, ex: include/assets/hero.h"
241
+ }
242
+ },
243
+ required: ["image_path", "output_path"]
244
+ }
245
+ },
246
+ ],
247
+ };
248
+ });
249
+
250
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
251
+ const { name, arguments: args } = request.params;
252
+
253
+ // --- Tool : semantic_search ---
254
+ if (name === "semantic_search") {
255
+ try {
256
+ const data = await semanticSearch({
257
+ query: args.query,
258
+ tags: args.tags,
259
+ maxResults: args.maxResults ?? 5,
260
+ minScore: args.minSimilarityScore ?? 0.7,
261
+ fallbackScore: 0.5,
262
+ });
263
+
264
+ const results = data.results || [];
265
+
266
+ if (results.length === 0) {
267
+ return {
268
+ content: [{ type: "text", text: "Aucun résultat trouvé, même avec un seuil de similarité abaissé à 0.5. Essayez d'autres mots-clés ou consultez list_official_docs." }]
269
+ };
270
+ }
271
+
272
+ const fallbackNote = data._usedFallback
273
+ ? `⚠️ Aucun résultat au seuil demandé — résultats obtenus avec un seuil abaissé à ${data._fallbackScore} (pertinence réduite).\n\n`
274
+ : "";
275
+
276
+ const text = fallbackNote + results
277
+ .map(r =>
278
+ `${r.content}\n` +
279
+ `(Source: ${r.document?.fileName}, score: ${r.similarityScore}, ` +
280
+ `chunkIndex: ${r.position?.chunkIndex ?? "?"}, documentId: ${r.document?.documentId})`
281
+ )
282
+ .join("\n\n");
283
+
284
+ return { content: [{ type: "text", text }] };
285
+
286
+ } catch (error) {
287
+ console.error("Erreur semantic_search:", error.message);
288
+ return {
289
+ content: [{ type: "text", text: `Désolé, le serveur de recherche ne répond pas après plusieurs tentatives (Erreur: ${error.message}).` }],
290
+ isError: true
291
+ };
292
+ }
293
+ }
294
+
295
+ // --- Tool : get_chunk_context ---
296
+ if (name === "get_chunk_context") {
297
+ try {
298
+ const { documentId, chunkIndex, contextSize = 2 } = args;
299
+
300
+ const res = await fetchWithRetry(`${RAG_BASE_URL}/api/Documents/${documentId}`, {
301
+ timeout: 15000
302
+ }, 3);
303
+
304
+ if (!res.ok) throw new Error(`Document introuvable (${res.status})`);
305
+
306
+ const doc = await res.json();
307
+ const chunks = doc.chunks ?? [];
308
+
309
+ if (chunks.length === 0) {
310
+ return { content: [{ type: "text", text: "Ce document ne contient aucun chunk accessible." }] };
311
+ }
312
+
313
+ const minIndex = Math.max(0, chunkIndex - contextSize);
314
+ const maxIndex = Math.min(chunks.length - 1, chunkIndex + contextSize);
315
+
316
+ const window = chunks
317
+ .filter(c => c.chunkIndex >= minIndex && c.chunkIndex <= maxIndex)
318
+ .sort((a, b) => a.chunkIndex - b.chunkIndex);
319
+
320
+ const text =
321
+ `Contexte élargi — document : ${doc.fileName} (chunks ${minIndex} à ${maxIndex})\n\n` +
322
+ window.map(c =>
323
+ `--- Chunk ${c.chunkIndex}` +
324
+ `${c.chunkIndex === chunkIndex ? " [chunk central]" : ""}` +
325
+ `${c.sectionHeading ? ` — ${c.sectionHeading}` : ""} ---\n${c.content}`
326
+ ).join("\n\n");
327
+
328
+ return { content: [{ type: "text", text }] };
329
+
330
+ } catch (error) {
331
+ console.error("Erreur get_chunk_context:", error.message);
332
+ return {
333
+ content: [{ type: "text", text: `Impossible de récupérer le contexte du chunk (Erreur: ${error.message}).` }],
334
+ isError: true
335
+ };
336
+ }
337
+ }
338
+
339
+ // --- Tool : list_official_docs ---
340
+ if (name === "list_official_docs") {
341
+ try {
342
+ const res = await fetchWithRetry(`${RAG_BASE_URL}/api/documents/official`, {
343
+ timeout: 15000
344
+ }, 3);
345
+
346
+ if (!res.ok) {
347
+ const errorText = await res.text();
348
+ throw new Error(`Erreur API NAS (${res.status}): ${errorText}`);
349
+ }
350
+
351
+ const docs = await res.json();
352
+
353
+ const filtered = args.tag
354
+ ? docs.filter(d => d.tags?.some(t => t.toLowerCase().includes(args.tag.toLowerCase())))
355
+ : docs;
356
+
357
+ const text = filtered.length > 0
358
+ ? filtered.map(d =>
359
+ `**${d.title ?? d.fileName}** (ID: ${d.id})\n` +
360
+ `Tags: ${(d.tags ?? []).join(", ") || "aucun"}\n` +
361
+ `Résumé: ${d.summary ?? "N/A"}\n` +
362
+ `📥 Télécharger: ${RAG_BASE_URL}/api/documents/official/${d.id}/file`
363
+ ).join("\n\n")
364
+ : "Aucun document officiel trouvé.";
365
+
366
+ return { content: [{ type: "text", text }] };
367
+
368
+ } catch (error) {
369
+ console.error("Erreur list_official_docs:", error.message);
370
+ return {
371
+ content: [{ type: "text", text: `Impossible de récupérer les documents officiels (Erreur: ${error.message}).` }],
372
+ isError: true
373
+ };
374
+ }
375
+ }
376
+
377
+ // --- Tool : make_fd ---
378
+ if (name === "make_fd") {
379
+ const { output_fd, input_bins } = args;
380
+
381
+ if (!Array.isArray(input_bins) || input_bins.length === 0) {
382
+ return {
383
+ content: [{ type: "text", text: "✗ Erreur : input_bins doit contenir au moins un fichier .BIN." }],
384
+ isError: true
385
+ };
386
+ }
387
+
388
+ try {
389
+ const { stdout } = await runScript("makefd.py", [output_fd, ...input_bins]);
390
+ return {
391
+ content: [{ type: "text", text: stdout.trim() || `✓ Image .fd générée : ${output_fd}` }]
392
+ };
393
+ } catch (error) {
394
+ console.error("Erreur make_fd:", error.message);
395
+ return {
396
+ content: [{ type: "text", text: `✗ Échec génération .fd : ${error.message}` }],
397
+ isError: true
398
+ };
399
+ }
400
+ }
401
+
402
+ // --- Tool : fd_to_sd ---
403
+ if (name === "fd_to_sd") {
404
+ const { input_fd, output_sd } = args;
405
+
406
+ try {
407
+ const { stdout } = await runScript("fd2sd.py", ["-conv", input_fd, output_sd]);
408
+ return {
409
+ content: [{ type: "text", text: stdout.trim() || `✓ Conversion terminée : ${output_sd}` }]
410
+ };
411
+ } catch (error) {
412
+ console.error("Erreur fd_to_sd:", error.message);
413
+ return {
414
+ content: [{ type: "text", text: `✗ Échec conversion .fd → .sd : ${error.message}` }],
415
+ isError: true
416
+ };
417
+ }
418
+ }
419
+
420
+ // --- Tool : png_to_mo5_sprite ---
421
+ if (name === "png_to_mo5_sprite") {
422
+ const { image_path, output_path } = args;
423
+
424
+ try {
425
+ const { stdout } = await runScript("png2mo5.py", [
426
+ image_path,
427
+ "--name", output_path,
428
+ "--quiet"
429
+ ]);
430
+ return {
431
+ content: [{ type: "text", text: stdout.trim() || `✓ Sprite généré : ${output_path}` }]
432
+ };
433
+ } catch (error) {
434
+ console.error("Erreur png_to_mo5_sprite:", error.message);
435
+ return {
436
+ content: [{ type: "text", text: `✗ Échec conversion PNG → sprite : ${error.message}` }],
437
+ isError: true
438
+ };
439
+ }
440
+ }
441
+
442
+ throw new Error(`Outil inconnu : ${name}`);
443
+ });
444
+
445
+ /* ============================================================
446
+ RESOURCES (Données consultables)
447
+ ============================================================ */
448
+
449
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
450
+ return {
451
+ resources: [
452
+ { uri: "documents://list", name: "Liste des documents", description: "Liste tous les fichiers indexés (chunks)" },
453
+ { uri: "documents://tags", name: "Tags", description: "Liste des thématiques disponibles" },
454
+ { uri: "documents://official", name: "Documentation officielle MO5", description: "Manuels, guides et docs de développement officiels Thomson MO5" },
455
+ { uri: "ingestion://status", name: "Statut", description: "État de santé de l'indexation RAG" },
456
+ ],
457
+ };
458
+ });
459
+
460
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
461
+ const uri = request.params.uri;
462
+
463
+ try {
464
+ let text = "";
465
+
466
+ if (uri === "documents://list") {
467
+ const res = await fetch(`${RAG_BASE_URL}/api/Documents`);
468
+ const docs = await res.json();
469
+ text = docs.map((d) => `- ${d.fileName} (ID: ${d.id})`).join("\n");
470
+ }
471
+ else if (uri === "documents://tags") {
472
+ const res = await fetch(`${RAG_BASE_URL}/api/Documents/tags`);
473
+ const tags = await res.json();
474
+ text = tags.map((t) => t.name).join(", ");
475
+ }
476
+ else if (uri === "documents://official") {
477
+ const res = await fetch(`${RAG_BASE_URL}/api/documents/official`);
478
+ const docs = await res.json();
479
+ text = docs.map(d =>
480
+ `## ${d.title ?? d.fileName}\n` +
481
+ `- ID: ${d.id}\n` +
482
+ `- Tags: ${(d.tags ?? []).join(", ") || "aucun"}\n` +
483
+ `- Résumé: ${d.summary ?? "N/A"}\n` +
484
+ `- Modifié: ${d.lastModified}\n` +
485
+ `- 📥 Télécharger: ${RAG_BASE_URL}/api/documents/official/${d.id}/file`
486
+ ).join("\n\n");
487
+ }
488
+ else if (uri.startsWith("documents://official/")) {
489
+ const id = uri.replace("documents://official/", "");
490
+ const res = await fetch(`${RAG_BASE_URL}/api/documents/official/${id}`);
491
+ if (!res.ok) throw new Error("Document officiel introuvable");
492
+ const doc = await res.json();
493
+ text =
494
+ `# ${doc.title ?? doc.fileName}\n\n` +
495
+ `${doc.summary ?? ""}\n\n` +
496
+ `Tags: ${(doc.tags ?? []).join(", ") || "aucun"}\n` +
497
+ `Fichier: ${doc.fileName}\n` +
498
+ `Modifié: ${doc.lastModified}\n` +
499
+ `📥 Télécharger: ${RAG_BASE_URL}/api/documents/official/${doc.id}/file`;
500
+ }
501
+ else if (uri === "ingestion://status") {
502
+ const res = await fetch(`${RAG_BASE_URL}/api/Index/status`);
503
+ const status = await res.json();
504
+ text = JSON.stringify(status, null, 2);
505
+ }
506
+ else if (uri.startsWith("documents://")) {
507
+ const id = uri.replace("documents://", "");
508
+ const res = await fetch(`${RAG_BASE_URL}/api/Documents/${id}`);
509
+ if (!res.ok) throw new Error("Document introuvable");
510
+ const doc = await res.json();
511
+ text = `# ${doc.title}\n\n${doc.content}\n\nSource: ${doc.fileName}`;
512
+ }
513
+ else {
514
+ throw new Error("Ressource inconnue");
515
+ }
516
+
517
+ return {
518
+ contents: [
519
+ {
520
+ uri: uri,
521
+ mimeType: "text/plain",
522
+ text: text
523
+ }
524
+ ]
525
+ };
526
+
527
+ } catch (error) {
528
+ throw new Error(`Erreur ressource (${uri}) : ${error.message}`);
529
+ }
530
+ });
531
+
532
+ /* ============================================================
533
+ PROMPTS (Modèles de conversation)
534
+ ============================================================ */
535
+
536
+ server.setRequestHandler(ListPromptsRequestSchema, async () => {
537
+ return {
538
+ prompts: [
539
+ {
540
+ name: "mo5_expert",
541
+ description: "Assistant expert Thomson MO5 utilisant la base documentaire.",
542
+ },
543
+ ],
544
+ };
545
+ });
546
+
547
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
548
+ if (request.params.name !== "mo5_expert") {
549
+ throw new Error("Prompt inconnu");
550
+ }
551
+
552
+ return {
553
+ description: "Expert MO5",
554
+ messages: [
555
+ {
556
+ role: "user",
557
+ content: {
558
+ type: "text",
559
+ text: `Tu es un expert du micro-ordinateur Thomson MO5.
560
+ Tu as accès aux outils suivants, à utiliser dans cet ordre selon le besoin :
561
+
562
+ **Documentation (RAG)**
563
+ 1. **list_official_docs** : commence par cet outil quand l'utilisateur cherche une référence précise (manuel BASIC, doc du 6809, format disquette...). Il liste les documents officiels avec résumés, tags et liens de téléchargement.
564
+ 2. **semantic_search** : recherche sémantique dans les chunks indexés. Utilise les tags identifiés via list_official_docs pour affiner. Un fallback automatique est appliqué si aucun résultat n'est trouvé au seuil demandé.
565
+ 3. **get_chunk_context** : si un résultat de semantic_search semble incomplet ou tronqué, utilise cet outil pour récupérer les chunks voisins du même document (documentId + chunkIndex sont fournis dans chaque résultat).
566
+
567
+ **Build & outils MO5**
568
+ 4. **make_fd** : génère une image disquette .fd bootable à partir d'un ou plusieurs .BIN compilés avec CMOC. Utilise-le après la compilation pour créer l'image disquette.
569
+ 5. **fd_to_sd** : convertit le .fd en .sd pour SDDrive. Utilise-le après make_fd si le fichier est destiné à une carte SD.
570
+ 6. **png_to_mo5_sprite** : convertit un PNG en fichier .h C (tableaux FORME + COULEUR) pour afficher des sprites avec le SDK MO5.
571
+
572
+ Cite toujours tes sources (fileName, score de similarité) et propose les liens de téléchargement quand c'est pertinent.`
573
+ }
574
+ }
575
+ ]
576
+ };
577
+ });
578
+
579
+ /* ============================================================
580
+ DÉMARRAGE DU SERVEUR
581
+ ============================================================ */
582
+
583
+ async function main() {
584
+ const transport = new StdioServerTransport();
585
+ await server.connect(transport);
586
+ console.error("Serveur MCP MO5-RAG démarré sur STDIO");
587
+ }
588
+
589
+ main().catch((error) => {
590
+ console.error("Erreur fatale au démarrage:", error);
591
+ process.exit(1);
592
+ });
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@thlg057/mo5-rag-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Serveur MCP pour le rétrocomputing Thomson MO5 (RAG et outils de build)",
5
+ "type": "module",
6
+ "bin": {
7
+ "mo5-rag-mcp": "./index.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ "scripts/",
12
+ "package.json"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/thlg057/mo5-mcp-server.git"
17
+ },
18
+ "keywords": [
19
+ "mcp",
20
+ "mo5",
21
+ "thomson",
22
+ "retrocomputing",
23
+ "rag",
24
+ "cmoc",
25
+ "motorola-6809"
26
+ ],
27
+ "author": "Thierry (thlg057)",
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "@modelcontextprotocol/sdk": "^1.0.0"
31
+ }
32
+ }
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Convertit une image disquette .fd (3.5" 720Ko) en .sd (5.25" 320Ko)
4
+ pour Thomson MO5/TO7
5
+ Basé sur le code C d'OlivierP-To8
6
+ https://github.com/OlivierP-To8/InufutoPorts/blob/main/Thomson/fdtosd.c
7
+ """
8
+
9
+ import sys
10
+ import os
11
+
12
+ def fd_to_sd(fd_path, sd_path):
13
+ """Convertit un fichier .fd en .sd"""
14
+
15
+ sector_buffer = bytearray(256)
16
+ empty = bytes([0xFF] * 512)
17
+
18
+ try:
19
+ with open(fd_path, 'rb') as fd, open(sd_path, 'wb') as sd:
20
+ # 4 faces * 80 pistes = 320 pistes
21
+ for track in range(4 * 80):
22
+ # 16 secteurs par piste
23
+ for sector in range(1, 17):
24
+ data = fd.read(256)
25
+ if len(data) == 256:
26
+ # Écrire le secteur + padding vide
27
+ sd.write(data)
28
+ sd.write(empty[:256])
29
+ else:
30
+ # Secteur manquant = remplir de 0xFF
31
+ sd.write(empty)
32
+
33
+ print(f"✓ Conversion réussie: {sd_path}")
34
+ return True
35
+
36
+ except FileNotFoundError:
37
+ print(f"✗ Erreur: impossible d'ouvrir {fd_path}")
38
+ return False
39
+ except Exception as e:
40
+ print(f"✗ Erreur lors de la conversion: {e}")
41
+ return False
42
+
43
+ def main():
44
+ if len(sys.argv) != 4 or sys.argv[1] != '-conv':
45
+ print(f"Usage: {sys.argv[0]} -conv disk.fd disk.sd")
46
+ sys.exit(1)
47
+
48
+ fd_path = sys.argv[2]
49
+ sd_path = sys.argv[3]
50
+
51
+ if not os.path.exists(fd_path):
52
+ print(f"✗ Erreur: le fichier {fd_path} n'existe pas")
53
+ sys.exit(1)
54
+
55
+ success = fd_to_sd(fd_path, sd_path)
56
+ sys.exit(0 if success else 1)
57
+
58
+ if __name__ == '__main__':
59
+ main()