@rgaaudit/mcp-server 0.3.0 → 0.4.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/README.md +17 -16
- package/dist/index.d.ts +1 -1
- package/dist/index.js +336 -134
- package/package.json +50 -52
- package/dist/local-scanner.d.ts +0 -66
- package/dist/local-scanner.js +0 -156
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @rgaaudit/mcp-server
|
|
2
2
|
|
|
3
|
-
Serveur MCP (Model Context Protocol) pour auditer l'
|
|
3
|
+
Serveur MCP (Model Context Protocol) pour auditer l'accessibilité web selon le référentiel français **RGAA 4.1**, directement depuis votre IDE.
|
|
4
4
|
|
|
5
5
|
Compatible **Claude Code**, **Cursor**, **Copilot**, **Windsurf**.
|
|
6
6
|
|
|
@@ -12,15 +12,16 @@ npx @rgaaudit/mcp-server
|
|
|
12
12
|
|
|
13
13
|
## Configuration
|
|
14
14
|
|
|
15
|
-
### 1. Obtenir une
|
|
15
|
+
### 1. Obtenir une clé API
|
|
16
16
|
|
|
17
|
-
1.
|
|
18
|
-
2.
|
|
19
|
-
3.
|
|
17
|
+
1. Créez un compte sur [rgaaudit.fr](https://rgaaudit.fr)
|
|
18
|
+
2. Choisissez un plan **Freelance** ou supérieur (le plan Découverte ne donne pas accès à l'API)
|
|
19
|
+
3. Allez dans **Paramètres > API**
|
|
20
|
+
4. Générez une clé API (préfixe `rga_`)
|
|
20
21
|
|
|
21
22
|
### 2. Configurer Claude Code
|
|
22
23
|
|
|
23
|
-
Ajoutez dans `.mcp.json`
|
|
24
|
+
Ajoutez dans `.mcp.json` à la racine de votre projet :
|
|
24
25
|
|
|
25
26
|
```json
|
|
26
27
|
{
|
|
@@ -38,35 +39,35 @@ Ajoutez dans `.mcp.json` a la racine de votre projet :
|
|
|
38
39
|
|
|
39
40
|
### 3. Configurer Cursor / Copilot
|
|
40
41
|
|
|
41
|
-
Ajoutez la
|
|
42
|
+
Ajoutez la même configuration dans les paramètres MCP de votre IDE.
|
|
42
43
|
|
|
43
44
|
## Outils disponibles
|
|
44
45
|
|
|
45
46
|
| Outil | Description |
|
|
46
47
|
|-------|-------------|
|
|
47
48
|
| `rgaa_audit` | Audit complet RGAA 4.1 avec violations, patterns et suggestions |
|
|
48
|
-
| `rgaa_score` | Score de
|
|
49
|
-
| `rgaa_check` |
|
|
50
|
-
| `rgaa_explain` | Expliquer un
|
|
51
|
-
| `rgaa_report` | Rapport JSON
|
|
49
|
+
| `rgaa_score` | Score de conformité rapide (0-100%) |
|
|
50
|
+
| `rgaa_check` | Vérifier un critère RGAA spécifique (ex: 1.1, 3.2, 11.1) |
|
|
51
|
+
| `rgaa_explain` | Expliquer un critère RGAA avec exemples de code |
|
|
52
|
+
| `rgaa_report` | Rapport JSON structuré pour CI/CD |
|
|
52
53
|
|
|
53
54
|
## Exemples d'utilisation
|
|
54
55
|
|
|
55
56
|
Dans Claude Code, demandez simplement :
|
|
56
57
|
|
|
57
58
|
```
|
|
58
|
-
> Audite l'
|
|
59
|
+
> Audite l'accessibilité de https://monsite.fr
|
|
59
60
|
> Quel est le score RGAA de https://monsite.fr ?
|
|
60
|
-
>
|
|
61
|
-
> Explique le
|
|
61
|
+
> Vérifie le critère 3.2 (contrastes) sur https://monsite.fr
|
|
62
|
+
> Explique le critère RGAA 11.1
|
|
62
63
|
```
|
|
63
64
|
|
|
64
65
|
## Variables d'environnement
|
|
65
66
|
|
|
66
67
|
| Variable | Requis | Description |
|
|
67
68
|
|----------|--------|-------------|
|
|
68
|
-
| `RGAA_API_KEY` | Oui |
|
|
69
|
-
| `RGAA_API_URL` | Non | URL de l'API (
|
|
69
|
+
| `RGAA_API_KEY` | Oui | Clé API RGAAudit (format `rga_...`) |
|
|
70
|
+
| `RGAA_API_URL` | Non | URL de l'API (défaut: `https://rgaaudit.fr`) |
|
|
70
71
|
|
|
71
72
|
## Licence
|
|
72
73
|
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Configuration requise :
|
|
9
9
|
* RGAA_API_KEY — Clé API (générée dans Paramètres > API)
|
|
10
|
-
* RGAA_API_URL — URL de l'API (optionnel, défaut: https://
|
|
10
|
+
* RGAA_API_URL — URL de l'API (optionnel, défaut: https://rgaaudit.fr)
|
|
11
11
|
*
|
|
12
12
|
* Outils disponibles :
|
|
13
13
|
* - rgaa_audit : Lancer un audit complet sur une URL
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Configuration requise :
|
|
9
9
|
* RGAA_API_KEY — Clé API (générée dans Paramètres > API)
|
|
10
|
-
* RGAA_API_URL — URL de l'API (optionnel, défaut: https://
|
|
10
|
+
* RGAA_API_URL — URL de l'API (optionnel, défaut: https://rgaaudit.fr)
|
|
11
11
|
*
|
|
12
12
|
* Outils disponibles :
|
|
13
13
|
* - rgaa_audit : Lancer un audit complet sur une URL
|
|
@@ -19,12 +19,11 @@
|
|
|
19
19
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
20
20
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
21
21
|
import { z } from 'zod';
|
|
22
|
-
|
|
23
|
-
const API_URL = process.env.RGAA_API_URL || 'https://app.rgaaudit.fr';
|
|
22
|
+
const API_URL = process.env.RGAA_API_URL || 'https://rgaaudit.fr';
|
|
24
23
|
const API_KEY = process.env.RGAA_API_KEY || '';
|
|
25
24
|
const server = new McpServer({
|
|
26
25
|
name: 'rgaaudit',
|
|
27
|
-
version: '0.3.
|
|
26
|
+
version: '0.3.1',
|
|
28
27
|
});
|
|
29
28
|
// ── Cache d'audit (TTL 10 min, évite double consommation de quota) ──
|
|
30
29
|
const CACHE_TTL_MS = 10 * 60 * 1000;
|
|
@@ -40,107 +39,79 @@ function cleanCache() {
|
|
|
40
39
|
}
|
|
41
40
|
}
|
|
42
41
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* @param url - URL locale à scanner
|
|
47
|
-
* @param options - details pour inclure les nœuds HTML
|
|
48
|
-
*/
|
|
49
|
-
async function callLocalScan(url, options) {
|
|
50
|
-
const localResult = await scanLocal(url);
|
|
51
|
-
// Enrichir avec le mapping RGAA si API disponible
|
|
52
|
-
const ruleIds = localResult.violations.map(v => v.ruleId);
|
|
53
|
-
const mapping = API_KEY ? await fetchRgaaMappings(ruleIds, API_URL, API_KEY) : null;
|
|
54
|
-
const violations = localResult.violations;
|
|
55
|
-
const score = violations.length === 0 ? 100 : Math.max(0, Math.round(100 - (violations.length * 3)));
|
|
56
|
-
const conformity = score >= 100 ? 'conforme' : score >= 50 ? 'partiellement_conforme' : 'non_conforme';
|
|
57
|
-
const threshold = options?.threshold ?? 0;
|
|
58
|
-
const result = {
|
|
59
|
-
url,
|
|
60
|
-
score,
|
|
61
|
-
conformity,
|
|
62
|
-
violations: violations.length,
|
|
63
|
-
pages: 1,
|
|
64
|
-
pass: threshold > 0 ? score >= threshold : true,
|
|
65
|
-
threshold: threshold > 0 ? threshold : null,
|
|
66
|
-
auditId: `local-${Date.now()}`,
|
|
67
|
-
auditUrl: url,
|
|
68
|
-
coverage: mapping?.coverage,
|
|
69
|
-
};
|
|
70
|
-
if (options?.details) {
|
|
71
|
-
result.violationDetails = violations.map(v => {
|
|
72
|
-
const m = mapping?.mappings[v.ruleId];
|
|
73
|
-
return {
|
|
74
|
-
ruleId: v.ruleId,
|
|
75
|
-
criteria: m?.criteria || null,
|
|
76
|
-
criteriaTitle: m?.title || null,
|
|
77
|
-
theme: m?.theme || null,
|
|
78
|
-
description: m?.help || v.description,
|
|
79
|
-
impact: m?.impact || v.impact,
|
|
80
|
-
nodesCount: v.nodesCount,
|
|
81
|
-
zone: null,
|
|
82
|
-
suggestion: m?.help || v.help,
|
|
83
|
-
nodes: v.nodes,
|
|
84
|
-
};
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
return result;
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Appelle POST /api/ci/scan (distant) ou scanLocal (localhost).
|
|
91
|
-
* Cache intelligent : réutilise l'auditId si un audit récent existe.
|
|
42
|
+
* Appelle POST /api/ci/scan — toujours via l'API (règles custom + patterns + mapping RGAA).
|
|
43
|
+
* Cache intelligent : réutilise l'auditId si un audit récent existe (0 quota).
|
|
92
44
|
*
|
|
93
45
|
* @param url - URL à auditer
|
|
94
46
|
* @param options - details, threshold, auditId (pour réutiliser un audit existant)
|
|
95
47
|
*/
|
|
96
48
|
async function callScan(url, options) {
|
|
49
|
+
if (!API_KEY) {
|
|
50
|
+
throw new Error('RGAA_API_KEY non configurée.\n\n' +
|
|
51
|
+
'1. Créez un compte sur https://rgaaudit.fr\n' +
|
|
52
|
+
'2. Allez dans Paramètres > API\n' +
|
|
53
|
+
'3. Générez une clé API\n' +
|
|
54
|
+
'4. Configurez : RGAA_API_KEY=rga_...');
|
|
55
|
+
}
|
|
97
56
|
cleanCache();
|
|
98
|
-
// Vérifier le cache
|
|
57
|
+
// Vérifier le cache : si un audit récent existe, réutiliser l'auditId (0 quota)
|
|
99
58
|
const cacheKey = url.toLowerCase().replace(/\/+$/, '');
|
|
100
59
|
const cached = auditCache.get(cacheKey);
|
|
101
60
|
if (cached && !options?.auditId) {
|
|
102
|
-
// Cache hit — retourner directement si on a déjà les détails ou si on n'en demande pas
|
|
103
61
|
if (!options?.details || cached.result.violationDetails) {
|
|
104
62
|
return cached.result;
|
|
105
63
|
}
|
|
106
64
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
'X-API-Key': API_KEY,
|
|
126
|
-
},
|
|
127
|
-
body: JSON.stringify({
|
|
128
|
-
url,
|
|
129
|
-
details: options?.details ?? false,
|
|
130
|
-
threshold: options?.threshold ?? 0,
|
|
131
|
-
...(auditId ? { auditId } : {}),
|
|
132
|
-
}),
|
|
133
|
-
});
|
|
134
|
-
if (!res.ok) {
|
|
135
|
-
const body = await res.json().catch(() => ({}));
|
|
136
|
-
throw new Error(`API erreur ${res.status}: ${body.message || res.statusText}`);
|
|
137
|
-
}
|
|
138
|
-
result = await res.json();
|
|
65
|
+
const auditId = options?.auditId || cached?.result.auditId;
|
|
66
|
+
const res = await fetch(`${API_URL}/api/ci/scan`, {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: {
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
'X-API-Key': API_KEY,
|
|
71
|
+
'X-Source': 'mcp',
|
|
72
|
+
},
|
|
73
|
+
body: JSON.stringify({
|
|
74
|
+
url,
|
|
75
|
+
details: options?.details ?? false,
|
|
76
|
+
threshold: options?.threshold ?? 0,
|
|
77
|
+
...(auditId ? { auditId } : {}),
|
|
78
|
+
}),
|
|
79
|
+
});
|
|
80
|
+
if (!res.ok) {
|
|
81
|
+
const body = await res.json().catch(() => ({}));
|
|
82
|
+
throw new Error(`API erreur ${res.status}: ${body.message || res.statusText}`);
|
|
139
83
|
}
|
|
84
|
+
const result = await res.json();
|
|
140
85
|
// Mettre en cache
|
|
141
86
|
auditCache.set(cacheKey, { result, timestamp: Date.now() });
|
|
142
87
|
return result;
|
|
143
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Appelle POST /api/ci/recheck — re-scanne les mêmes pages d'un audit existant.
|
|
91
|
+
* Smart re-audit : pas de discovery, pas de crawl. Consomme 1 token.
|
|
92
|
+
* Retourne le delta (score, violations résolues/nouvelles).
|
|
93
|
+
*
|
|
94
|
+
* @param auditId - ID de l'audit à re-checker
|
|
95
|
+
*/
|
|
96
|
+
async function callRecheck(auditId) {
|
|
97
|
+
if (!API_KEY) {
|
|
98
|
+
throw new Error('RGAA_API_KEY non configurée.');
|
|
99
|
+
}
|
|
100
|
+
const res = await fetch(`${API_URL}/api/ci/recheck`, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: {
|
|
103
|
+
'Content-Type': 'application/json',
|
|
104
|
+
'X-API-Key': API_KEY,
|
|
105
|
+
'X-Source': 'mcp',
|
|
106
|
+
},
|
|
107
|
+
body: JSON.stringify({ auditId }),
|
|
108
|
+
});
|
|
109
|
+
if (!res.ok) {
|
|
110
|
+
const body = await res.json().catch(() => ({}));
|
|
111
|
+
throw new Error(`API erreur ${res.status}: ${body.message || res.statusText}`);
|
|
112
|
+
}
|
|
113
|
+
return res.json();
|
|
114
|
+
}
|
|
144
115
|
// ── Outil 1 : rgaa_audit ──
|
|
145
116
|
server.tool('rgaa_audit', 'Lancer un audit d\'accessibilité RGAA 4.1 complet sur une URL. Retourne les violations avec critères RGAA, impact, et suggestions de correction.', {
|
|
146
117
|
url: z.string().url().describe('URL du site web à auditer'),
|
|
@@ -148,48 +119,126 @@ server.tool('rgaa_audit', 'Lancer un audit d\'accessibilité RGAA 4.1 complet su
|
|
|
148
119
|
try {
|
|
149
120
|
const result = await callScan(url, { details: true });
|
|
150
121
|
const violations = result.violationDetails || [];
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
122
|
+
const impactLabels = { critical: 'critique', serious: 'sérieux', moderate: 'modéré', minor: 'mineur', critique: 'critique', serieux: 'sérieux', modere: 'modéré' };
|
|
123
|
+
const impactOrder = { critical: 0, critique: 0, serious: 1, serieux: 1, moderate: 2, modere: 2, minor: 3, mineur: 3 };
|
|
124
|
+
const bySeverity = {};
|
|
125
|
+
for (const v of violations) {
|
|
126
|
+
const label = impactLabels[v.impact] || v.impact;
|
|
127
|
+
bySeverity[label] = (bySeverity[label] || 0) + 1;
|
|
128
|
+
}
|
|
157
129
|
const sharedInfo = result.violationsInSharedZones
|
|
158
130
|
? ` (dont ${result.violationsInSharedZones} dans des zones partagées nav/header/footer)`
|
|
159
131
|
: '';
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
132
|
+
// ── En-tête ──
|
|
133
|
+
let text = `# Audit RGAA 4.1 — ${url}\n\n`;
|
|
134
|
+
text += `| Métrique | Valeur |\n|----------|--------|\n`;
|
|
135
|
+
text += `| Score | **${result.score}%** — ${result.conformity} |\n`;
|
|
136
|
+
text += `| Non-conformités | ${result.violations}${sharedInfo} |\n`;
|
|
137
|
+
text += `| Par impact | ${Object.entries(bySeverity).map(([k, v]) => `${v} ${k}`).join(', ') || 'aucune'} |\n`;
|
|
138
|
+
text += `| Pages analysées | ${result.pages} auditées |\n`;
|
|
164
139
|
if (result.coverage) {
|
|
165
|
-
text +=
|
|
140
|
+
text += `| Couverture RGAA | ${result.coverage.percentage}% (${result.coverage.mappedCriteria}/${result.coverage.totalCriteria} critères) |\n`;
|
|
141
|
+
}
|
|
142
|
+
if (result.techStack?.framework || result.techStack?.cms) {
|
|
143
|
+
text += `| Technologies | ${[result.techStack.framework, result.techStack.cms].filter(Boolean).join(', ')} |\n`;
|
|
166
144
|
}
|
|
145
|
+
text += `| Source | ${result.source || 'ci'} |\n`;
|
|
146
|
+
text += `| Audit ID | \`${result.auditId}\` |\n`;
|
|
167
147
|
text += '\n';
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
148
|
+
// ── Pages auditées ──
|
|
149
|
+
if (result.pagesAudited && result.pagesAudited.length > 0) {
|
|
150
|
+
const audited = result.pagesAudited.filter(p => p.status === 'COMPLETED');
|
|
151
|
+
const extrapolated = result.pagesAudited.filter(p => p.status === 'EXTRAPOLATED');
|
|
152
|
+
const errors = result.pagesAudited.filter(p => !['COMPLETED', 'EXTRAPOLATED'].includes(p.status));
|
|
153
|
+
text += `## Pages (${result.pagesAudited.length} total : ${audited.length} auditées, ${extrapolated.length} extrapolées${errors.length ? `, ${errors.length} erreurs` : ''})\n\n`;
|
|
154
|
+
if (audited.length > 0) {
|
|
155
|
+
text += `### Pages auditées (axe-core)\n`;
|
|
156
|
+
for (const p of audited) {
|
|
157
|
+
text += `- **${p.title || p.url}** — ${p.url}${p.score != null ? ` — **${p.score}%**` : ''}\n`;
|
|
158
|
+
}
|
|
159
|
+
text += '\n';
|
|
160
|
+
}
|
|
161
|
+
if (extrapolated.length > 0) {
|
|
162
|
+
text += `### Pages extrapolées (Template Detection — mêmes violations que la page source)\n`;
|
|
163
|
+
for (const p of extrapolated) {
|
|
164
|
+
text += `- ${p.title || p.url} — ${p.url}\n`;
|
|
165
|
+
}
|
|
166
|
+
text += '\n';
|
|
172
167
|
}
|
|
173
|
-
text += '\n';
|
|
174
168
|
}
|
|
169
|
+
// ── Violations ──
|
|
175
170
|
if (violations.length > 0) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (v.
|
|
187
|
-
text +=
|
|
171
|
+
const real = violations.filter(v => !v.isExtrapolated);
|
|
172
|
+
const extrapolatedV = violations.filter(v => v.isExtrapolated);
|
|
173
|
+
// Trier par impact (critique d'abord)
|
|
174
|
+
real.sort((a, b) => (impactOrder[a.impact] ?? 4) - (impactOrder[b.impact] ?? 4));
|
|
175
|
+
text += `## Non-conformités (${real.length} réelles${extrapolatedV.length > 0 ? `, ${extrapolatedV.length} extrapolées` : ''})\n\n`;
|
|
176
|
+
for (const [i, v] of real.entries()) {
|
|
177
|
+
const impact = impactLabels[v.impact] || v.impact;
|
|
178
|
+
text += `### ${i + 1}. [${impact.toUpperCase()}] ${v.criteria || ''} — ${v.criteriaTitle || v.description}\n\n`;
|
|
179
|
+
text += `| | |\n|---|---|\n`;
|
|
180
|
+
text += `| Critère RGAA | ${v.criteria || 'N/A'} |\n`;
|
|
181
|
+
if (v.theme)
|
|
182
|
+
text += `| Thématique | ${v.theme} |\n`;
|
|
183
|
+
text += `| Impact | ${impact} |\n`;
|
|
184
|
+
text += `| Nœuds affectés | ${v.nodesCount} |\n`;
|
|
185
|
+
if (v.zone) {
|
|
186
|
+
const isShared = ['header', 'nav', 'footer'].includes(v.zone);
|
|
187
|
+
text += `| Zone | ${v.zone}${isShared ? ' **(partagée — corrigez le layout, pas chaque page)**' : ''} |\n`;
|
|
188
|
+
}
|
|
189
|
+
if (v.pageUrls && v.pageUrls.length > 0) {
|
|
190
|
+
text += `| Pages | ${v.pageUrls.join(', ')} |\n`;
|
|
191
|
+
}
|
|
192
|
+
text += '\n';
|
|
193
|
+
// Suggestion de correction
|
|
194
|
+
if (v.suggestion) {
|
|
195
|
+
text += `**Suggestion** : ${v.suggestion}\n\n`;
|
|
196
|
+
}
|
|
197
|
+
// Nœuds détaillés avec HTML
|
|
198
|
+
text += `**Nœuds affectés :**\n\n`;
|
|
199
|
+
for (const n of v.nodes.slice(0, 5)) {
|
|
200
|
+
const selector = n.contextPath || n.target;
|
|
201
|
+
text += `- **Sélecteur** : \`${selector}\`\n`;
|
|
202
|
+
if (n.pageUrl)
|
|
203
|
+
text += ` - Page : ${n.pageUrl}\n`;
|
|
204
|
+
if (n.html)
|
|
205
|
+
text += ` - HTML : \`${n.html.replace(/\n/g, ' ').trim()}\`\n`;
|
|
206
|
+
if (n.failureSummary)
|
|
207
|
+
text += ` - Problème : ${n.failureSummary}\n`;
|
|
208
|
+
if (n.zone)
|
|
209
|
+
text += ` - Zone : ${n.zone}\n`;
|
|
210
|
+
}
|
|
211
|
+
if (v.nodes.length > 5)
|
|
212
|
+
text += `- ... et ${v.nodes.length - 5} autres nœuds\n`;
|
|
213
|
+
text += '\n';
|
|
214
|
+
}
|
|
215
|
+
if (extrapolatedV.length > 0) {
|
|
216
|
+
text += `## Violations extrapolées (${extrapolatedV.length})\n\n`;
|
|
217
|
+
text += `> Template Detection : ces violations sont héritées d'une page source. Corrigez la page source, toutes les pages extrapolées seront corrigées.\n\n`;
|
|
218
|
+
for (const v of extrapolatedV) {
|
|
219
|
+
const impact = impactLabels[v.impact] || v.impact;
|
|
220
|
+
text += `- **[${impact}] ${v.criteria || ''}** ${v.criteriaTitle || v.description} — ${v.nodesCount} nœuds`;
|
|
221
|
+
if (v.pageUrls && v.pageUrls.length > 0)
|
|
222
|
+
text += ` (${v.pageUrls.join(', ')})`;
|
|
223
|
+
text += '\n';
|
|
188
224
|
}
|
|
189
225
|
text += '\n';
|
|
190
226
|
}
|
|
191
227
|
}
|
|
192
|
-
|
|
228
|
+
// ── Patterns / Corrections groupées ──
|
|
229
|
+
if (result.patterns && result.patterns.length > 0) {
|
|
230
|
+
text += `## Patterns de correction groupée (${result.patterns.length})\n\n`;
|
|
231
|
+
text += `> Un pattern = plusieurs nœuds identiques corrigeables en une seule modification.\n\n`;
|
|
232
|
+
text += `| Pattern | Type | Occurrences |\n|---------|------|-------------|\n`;
|
|
233
|
+
for (const p of result.patterns) {
|
|
234
|
+
text += `| ${p.title} | ${p.type} | ${p.occurrences} |\n`;
|
|
235
|
+
}
|
|
236
|
+
text += '\n';
|
|
237
|
+
}
|
|
238
|
+
// ── Lien rapport ──
|
|
239
|
+
text += `---\n\n`;
|
|
240
|
+
text += `**Rapport complet** : ${result.auditUrl}\n`;
|
|
241
|
+
text += `**Audit ID** : \`${result.auditId}\` (utilisable avec rgaa_recheck pour mesurer les améliorations)\n`;
|
|
193
242
|
return { content: [{ type: 'text', text }] };
|
|
194
243
|
}
|
|
195
244
|
catch (error) {
|
|
@@ -431,7 +480,6 @@ server.tool('rgaa_remediate', 'Plan de remédiation autonome : audite, priorise
|
|
|
431
480
|
try {
|
|
432
481
|
const result = await callScan(url, { details: true });
|
|
433
482
|
const violations = result.violationDetails || [];
|
|
434
|
-
const isLocal = isLocalUrl(url);
|
|
435
483
|
if (violations.length === 0) {
|
|
436
484
|
return {
|
|
437
485
|
content: [{
|
|
@@ -483,19 +531,14 @@ server.tool('rgaa_remediate', 'Plan de remédiation autonome : audite, priorise
|
|
|
483
531
|
text += `| Corrigeables auto | ${fixable.length} |\n`;
|
|
484
532
|
text += `| Manuelles | ${manual.length} |\n`;
|
|
485
533
|
text += `| Fixes dans ce plan | ${toFix.length} |\n`;
|
|
486
|
-
|
|
487
|
-
text += `| Mode | **Localhost** (re-audits gratuits) |\n`;
|
|
488
|
-
}
|
|
489
|
-
else {
|
|
490
|
-
text += `| Mode | **Distant** (re-audit = 1 quota) |\n`;
|
|
491
|
-
}
|
|
534
|
+
text += `| Re-check | 1 token (smart re-audit, pas de re-discovery) |\n`;
|
|
492
535
|
text += '\n';
|
|
493
536
|
// Score estimé après corrections
|
|
494
537
|
const estimatedGain = Math.min(30, toFix.length * 3);
|
|
495
538
|
const estimatedScore = Math.min(100, result.score + estimatedGain);
|
|
496
539
|
text += `**Score estimé après corrections** : ~${estimatedScore}% (+${estimatedGain} points)\n\n`;
|
|
497
540
|
text += `---\n\n## Corrections à appliquer (${toFix.length})\n\n`;
|
|
498
|
-
text += `Appliquez chaque fix dans l'ordre. Après le dernier fix, lancez \`
|
|
541
|
+
text += `Appliquez chaque fix dans l'ordre. Après le dernier fix, lancez \`rgaa_recheck("${result.auditId}")\` pour mesurer le delta.\n\n`;
|
|
499
542
|
for (const [i, v] of toFix.entries()) {
|
|
500
543
|
const fix = RGAA_FIXES[v.ruleId];
|
|
501
544
|
const code = fix.fixes[framework] || fix.fixes['html'];
|
|
@@ -503,8 +546,12 @@ server.tool('rgaa_remediate', 'Plan de remédiation autonome : audite, priorise
|
|
|
503
546
|
text += `- **Problème** : ${fix.description}\n`;
|
|
504
547
|
text += `- **Impact** : ${v.impact}\n`;
|
|
505
548
|
text += `- **Éléments** : ${v.nodesCount} nœuds affectés\n`;
|
|
506
|
-
if (v.zone)
|
|
549
|
+
if (v.zone && ['header', 'nav', 'footer'].includes(v.zone)) {
|
|
550
|
+
text += `- **Zone partagée** : ${v.zone} → corrigez le composant layout (1 fix = toutes les pages)\n`;
|
|
551
|
+
}
|
|
552
|
+
else if (v.zone) {
|
|
507
553
|
text += `- **Zone** : ${v.zone}\n`;
|
|
554
|
+
}
|
|
508
555
|
// Exemples de nœuds à corriger
|
|
509
556
|
if (v.nodes.length > 0) {
|
|
510
557
|
text += `- **Éléments à corriger** :\n`;
|
|
@@ -537,13 +584,7 @@ server.tool('rgaa_remediate', 'Plan de remédiation autonome : audite, priorise
|
|
|
537
584
|
text += `1. Pour chaque fix ci-dessus, identifiez le fichier source concerné\n`;
|
|
538
585
|
text += `2. Appliquez le code correctif adapté au composant\n`;
|
|
539
586
|
text += `3. Après toutes les corrections, exécutez :\n`;
|
|
540
|
-
text += ` - \`
|
|
541
|
-
if (isLocal) {
|
|
542
|
-
text += ` - Re-audits **gratuits** (scan local, pas de quota)\n`;
|
|
543
|
-
}
|
|
544
|
-
else {
|
|
545
|
-
text += ` - Chaque re-audit **consomme 1 crédit** du quota utilisateur\n`;
|
|
546
|
-
}
|
|
587
|
+
text += ` - \`rgaa_recheck("${result.auditId}")\` pour mesurer le delta (1 token)\n`;
|
|
547
588
|
text += `4. Si le score < ${targetScore}%, relancez \`rgaa_remediate\` pour le cycle suivant\n`;
|
|
548
589
|
text += `5. Vérifiez visuellement avec \`browser_screenshot\` que le rendu n'est pas cassé\n`;
|
|
549
590
|
return { content: [{ type: 'text', text }] };
|
|
@@ -552,7 +593,168 @@ server.tool('rgaa_remediate', 'Plan de remédiation autonome : audite, priorise
|
|
|
552
593
|
return { content: [{ type: 'text', text: `Erreur : ${error instanceof Error ? error.message : String(error)}` }], isError: true };
|
|
553
594
|
}
|
|
554
595
|
});
|
|
555
|
-
// ── Outil 7 :
|
|
596
|
+
// ── Outil 7 : rgaa_recheck ──
|
|
597
|
+
server.tool('rgaa_recheck', 'Re-check rapide : re-scanne les mêmes pages d\'un audit existant sans redécouvrir le site. Retourne le delta (score avant/après, violations résolues/nouvelles). Consomme 1 token du plan. Utilisez après avoir corrigé des violations pour mesurer l\'amélioration.', {
|
|
598
|
+
auditId: z.string().describe('ID de l\'audit à re-checker (retourné par rgaa_audit ou rgaa_report)'),
|
|
599
|
+
}, async ({ auditId }) => {
|
|
600
|
+
try {
|
|
601
|
+
const result = await callRecheck(auditId);
|
|
602
|
+
const deltaSign = result.score > result.previousScore ? '+' : '';
|
|
603
|
+
const violFixedText = result.violationsDelta > 0
|
|
604
|
+
? `${result.violationsDelta} violation(s) corrigée(s)`
|
|
605
|
+
: result.violationsDelta < 0
|
|
606
|
+
? `${Math.abs(result.violationsDelta)} nouvelle(s) violation(s)`
|
|
607
|
+
: 'Même nombre de violations';
|
|
608
|
+
const text = `# Re-check RGAA — ${result.url}\n\n` +
|
|
609
|
+
`| Métrique | Avant | Après | Delta |\n` +
|
|
610
|
+
`|----------|-------|-------|-------|\n` +
|
|
611
|
+
`| Score | ${result.previousScore}% | ${result.score}% | ${deltaSign}${result.score - result.previousScore} |\n` +
|
|
612
|
+
`| Violations | ${result.previousViolations} | ${result.violations} | ${result.violationsDelta > 0 ? '-' : '+'}${Math.abs(result.violationsDelta)} |\n\n` +
|
|
613
|
+
`**${result.message}**\n\n` +
|
|
614
|
+
`${violFixedText}\n\n` +
|
|
615
|
+
`- Audit précédent : ${result.parentAuditId}\n` +
|
|
616
|
+
`- Nouvel audit : ${result.newAuditId}\n\n` +
|
|
617
|
+
`Pour voir le détail des violations restantes, lancez \`rgaa_audit\` avec l'URL ou consultez l'audit dans l'interface.`;
|
|
618
|
+
return { content: [{ type: 'text', text }] };
|
|
619
|
+
}
|
|
620
|
+
catch (error) {
|
|
621
|
+
return { content: [{ type: 'text', text: `Erreur : ${error instanceof Error ? error.message : String(error)}` }], isError: true };
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
// ── Outil 8 : rgaa_visual_check ──
|
|
625
|
+
/**
|
|
626
|
+
* Checklist de vérification visuelle structurée.
|
|
627
|
+
* Chaque étape dit à l'agent quoi faire avec Browser MCP et quoi vérifier.
|
|
628
|
+
*/
|
|
629
|
+
const VISUAL_CHECKS = [
|
|
630
|
+
{
|
|
631
|
+
id: 'skip-link',
|
|
632
|
+
criteria: '12.7',
|
|
633
|
+
title: 'Lien d\'évitement visible au focus',
|
|
634
|
+
steps: [
|
|
635
|
+
'browser_navigate vers l\'URL',
|
|
636
|
+
'browser_press_key("Tab") — 1 fois',
|
|
637
|
+
'browser_screenshot — capturer l\'état du focus',
|
|
638
|
+
],
|
|
639
|
+
lookFor: 'Un lien "Aller au contenu" doit apparaître en haut de page au premier Tab. S\'il est invisible ou absent, c\'est une violation.',
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
id: 'focus-visible',
|
|
643
|
+
criteria: '10.7',
|
|
644
|
+
title: 'Indicateur de focus visible sur les éléments interactifs',
|
|
645
|
+
steps: [
|
|
646
|
+
'browser_press_key("Tab") — 5 fois successives',
|
|
647
|
+
'browser_screenshot à chaque Tab',
|
|
648
|
+
],
|
|
649
|
+
lookFor: 'Chaque élément focusé (lien, bouton, champ) doit avoir un contour/outline visible. Si le focus est invisible (pas de ring, pas de bordure), c\'est une violation.',
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
id: 'contrast-visual',
|
|
653
|
+
criteria: '3.3',
|
|
654
|
+
title: 'Contraste visuel — zones potentiellement problématiques',
|
|
655
|
+
steps: [
|
|
656
|
+
'browser_screenshot de la page complète',
|
|
657
|
+
'Examiner le footer, les badges, les textes muted/secondaires',
|
|
658
|
+
],
|
|
659
|
+
lookFor: 'Texte gris clair sur fond blanc, texte clair sur fond coloré, badges/tags peu contrastés. Les zones footer et navigation sont souvent problématiques.',
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
id: 'responsive-320',
|
|
663
|
+
criteria: '10.11',
|
|
664
|
+
title: 'Responsive — pas de scroll horizontal à 320px',
|
|
665
|
+
steps: [
|
|
666
|
+
'browser_press_key avec Ctrl+Shift+M (toggle device mode) ou resize la fenêtre à 320px de large',
|
|
667
|
+
'browser_screenshot',
|
|
668
|
+
],
|
|
669
|
+
lookFor: 'Aucun contenu ne doit déborder horizontalement. Pas de barre de scroll horizontal. Le texte doit rester lisible.',
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
id: 'heading-hierarchy',
|
|
673
|
+
criteria: '9.1',
|
|
674
|
+
title: 'Hiérarchie de titres logique',
|
|
675
|
+
steps: [
|
|
676
|
+
'browser_snapshot — lire l\'arbre d\'accessibilité',
|
|
677
|
+
'Lister tous les headings (h1-h6) dans l\'ordre',
|
|
678
|
+
],
|
|
679
|
+
lookFor: 'Un seul h1, puis h2, h3 dans l\'ordre. Pas de saut (h1→h4). Pas de h1 manquant. Vérifier que le h1 décrit le contenu principal.',
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
id: 'images-alt',
|
|
683
|
+
criteria: '1.1',
|
|
684
|
+
title: 'Images informatives avec alternative textuelle',
|
|
685
|
+
steps: [
|
|
686
|
+
'browser_snapshot — chercher toutes les images dans l\'arbre d\'accessibilité',
|
|
687
|
+
],
|
|
688
|
+
lookFor: 'Chaque image informative doit avoir un alt descriptif. Les images décoratives doivent avoir alt="". Les icônes dans les boutons/liens doivent être aria-hidden avec un texte accessible sur le parent.',
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
id: 'form-labels',
|
|
692
|
+
criteria: '11.1',
|
|
693
|
+
title: 'Champs de formulaire avec étiquettes',
|
|
694
|
+
steps: [
|
|
695
|
+
'browser_snapshot — chercher les champs input/select/textarea',
|
|
696
|
+
],
|
|
697
|
+
lookFor: 'Chaque champ doit avoir un label associé (via for/id ou aria-label). Les placeholder seuls ne sont PAS des labels valides.',
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
id: 'new-window',
|
|
701
|
+
criteria: '13.2',
|
|
702
|
+
title: 'Liens ouvrant une nouvelle fenêtre',
|
|
703
|
+
steps: [
|
|
704
|
+
'browser_snapshot — chercher les liens avec target="_blank"',
|
|
705
|
+
],
|
|
706
|
+
lookFor: 'Chaque lien target="_blank" doit indiquer "(nouvelle fenêtre)" en texte, sr-only, ou via une icône avec aria-label.',
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
id: 'keyboard-nav',
|
|
710
|
+
criteria: '12.8',
|
|
711
|
+
title: 'Navigation clavier complète',
|
|
712
|
+
steps: [
|
|
713
|
+
'browser_press_key("Tab") — naviguer dans toute la page',
|
|
714
|
+
'browser_press_key("Enter") sur les boutons/liens',
|
|
715
|
+
'browser_press_key("Escape") pour fermer les modales',
|
|
716
|
+
],
|
|
717
|
+
lookFor: 'Tous les éléments interactifs doivent être atteignables au clavier. Pas de piège (impossible de sortir d\'un élément). L\'ordre de tabulation doit suivre l\'ordre visuel.',
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
id: 'landmarks',
|
|
721
|
+
criteria: '12.9',
|
|
722
|
+
title: 'Zones principales identifiées par des landmarks',
|
|
723
|
+
steps: [
|
|
724
|
+
'browser_snapshot — vérifier la structure de la page',
|
|
725
|
+
],
|
|
726
|
+
lookFor: 'La page doit avoir : header (banner), nav (navigation), main, footer (contentinfo). Chaque zone doit être un landmark sémantique.',
|
|
727
|
+
},
|
|
728
|
+
];
|
|
729
|
+
server.tool('rgaa_visual_check', 'Génère une checklist de vérification visuelle pour l\'agent AI. Chaque étape indique quoi faire avec Browser MCP (navigate, screenshot, snapshot, Tab) et quoi vérifier visuellement. Couvre les critères que axe-core ne peut pas détecter automatiquement.', {
|
|
730
|
+
url: z.string().url().describe('URL de la page à vérifier visuellement'),
|
|
731
|
+
checks: z.array(z.string()).optional().describe('Liste d\'IDs de vérifications à effectuer (défaut: toutes). IDs: skip-link, focus-visible, contrast-visual, responsive-320, heading-hierarchy, images-alt, form-labels, new-window, keyboard-nav, landmarks'),
|
|
732
|
+
}, async ({ url, checks }) => {
|
|
733
|
+
const selectedChecks = checks && checks.length > 0
|
|
734
|
+
? VISUAL_CHECKS.filter(c => checks.includes(c.id))
|
|
735
|
+
: VISUAL_CHECKS;
|
|
736
|
+
let text = `# Vérification visuelle RGAA — ${url}\n\n`;
|
|
737
|
+
text += `**${selectedChecks.length} vérifications** à effectuer avec Browser MCP.\n`;
|
|
738
|
+
text += `Chaque étape indique les actions Browser MCP à exécuter et ce qu'il faut vérifier.\n\n`;
|
|
739
|
+
text += `---\n\n`;
|
|
740
|
+
for (const [i, check] of selectedChecks.entries()) {
|
|
741
|
+
text += `## ${i + 1}. ${check.title} (RGAA ${check.criteria})\n\n`;
|
|
742
|
+
text += `**Actions** :\n`;
|
|
743
|
+
for (const step of check.steps) {
|
|
744
|
+
text += `1. \`${step}\`\n`;
|
|
745
|
+
}
|
|
746
|
+
text += `\n**Ce qu'il faut vérifier** : ${check.lookFor}\n\n`;
|
|
747
|
+
text += `---\n\n`;
|
|
748
|
+
}
|
|
749
|
+
text += `## Instructions pour l'agent\n\n`;
|
|
750
|
+
text += `1. Commencez par \`browser_navigate("${url}")\`\n`;
|
|
751
|
+
text += `2. Exécutez chaque vérification dans l'ordre\n`;
|
|
752
|
+
text += `3. Pour chaque problème trouvé, notez le critère RGAA et la description\n`;
|
|
753
|
+
text += `4. Après toutes les vérifications, résumez les problèmes trouvés\n`;
|
|
754
|
+
text += `5. Utilisez \`rgaa_fix\` pour obtenir le code correctif de chaque problème\n`;
|
|
755
|
+
return { content: [{ type: 'text', text }] };
|
|
756
|
+
});
|
|
757
|
+
// ── Outil 9 : rgaa_report ──
|
|
556
758
|
server.tool('rgaa_report', 'Génère un rapport d\'audit RGAA structuré en JSON pour intégration CI/CD ou export.', {
|
|
557
759
|
url: z.string().url().describe('URL du site web'),
|
|
558
760
|
threshold: z.number().min(0).max(100).default(0).describe('Score minimum pour pass/fail (0 = pas de seuil)'),
|
|
@@ -569,7 +771,7 @@ server.tool('rgaa_report', 'Génère un rapport d\'audit RGAA structuré en JSON
|
|
|
569
771
|
async function main() {
|
|
570
772
|
if (!API_KEY) {
|
|
571
773
|
console.error('⚠️ RGAA_API_KEY non configurée. Les outils d\'audit ne fonctionneront pas.');
|
|
572
|
-
console.error(' Générez une clé sur https://
|
|
774
|
+
console.error(' Générez une clé sur https://rgaaudit.fr > Paramètres > API');
|
|
573
775
|
}
|
|
574
776
|
const transport = new StdioServerTransport();
|
|
575
777
|
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,52 +1,50 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@rgaaudit/mcp-server",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP Server RGAA 4.1 — Audit d'accessibilité pour Claude Code, Cursor, Copilot",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "dist/index.js",
|
|
7
|
-
"bin": {
|
|
8
|
-
"rgaaudit-mcp": "dist/index.js"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
"dist",
|
|
12
|
-
"README.md"
|
|
13
|
-
],
|
|
14
|
-
"scripts": {
|
|
15
|
-
"build": "tsc",
|
|
16
|
-
"dev": "tsx src/index.ts",
|
|
17
|
-
"start": "node dist/index.js",
|
|
18
|
-
"prepublishOnly": "npm run build"
|
|
19
|
-
},
|
|
20
|
-
"dependencies": {
|
|
21
|
-
"@
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@rgaaudit/mcp-server",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "MCP Server RGAA 4.1 — Audit d'accessibilité pour Claude Code, Cursor, Copilot",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"rgaaudit-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"dev": "tsx src/index.ts",
|
|
17
|
+
"start": "node dist/index.js",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
22
|
+
"zod": "^3.25.3"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"tsx": "^4.19.4",
|
|
26
|
+
"typescript": "^5.8.3"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18.0.0"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"mcp",
|
|
33
|
+
"rgaa",
|
|
34
|
+
"accessibility",
|
|
35
|
+
"a11y",
|
|
36
|
+
"wcag",
|
|
37
|
+
"audit",
|
|
38
|
+
"france",
|
|
39
|
+
"rgaa4"
|
|
40
|
+
],
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "https://github.com/Foxbane-c/RGAAudit"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://rgaaudit.fr/docs/mcp",
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/dist/local-scanner.d.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Scanner local axe-core — Mode localhost
|
|
3
|
-
*
|
|
4
|
-
* Utilise puppeteer-core (Chrome existant) + @axe-core/puppeteer
|
|
5
|
-
* pour auditer des URLs localhost sans passer par l'API distante.
|
|
6
|
-
*
|
|
7
|
-
* Le mapping RGAA est ensuite récupéré via POST /api/ci/map.
|
|
8
|
-
*/
|
|
9
|
-
/** Résultat brut d'un scan local axe-core */
|
|
10
|
-
export interface LocalScanResult {
|
|
11
|
-
url: string;
|
|
12
|
-
violations: LocalViolation[];
|
|
13
|
-
passes: number;
|
|
14
|
-
incomplete: number;
|
|
15
|
-
timestamp: string;
|
|
16
|
-
}
|
|
17
|
-
export interface LocalViolation {
|
|
18
|
-
ruleId: string;
|
|
19
|
-
impact: string;
|
|
20
|
-
description: string;
|
|
21
|
-
help: string;
|
|
22
|
-
helpUrl: string;
|
|
23
|
-
nodesCount: number;
|
|
24
|
-
nodes: Array<{
|
|
25
|
-
html: string;
|
|
26
|
-
target: string;
|
|
27
|
-
}>;
|
|
28
|
-
}
|
|
29
|
-
/** Mapping RGAA retourné par /api/ci/map */
|
|
30
|
-
export interface RgaaMapping {
|
|
31
|
-
criteria: string;
|
|
32
|
-
title: string;
|
|
33
|
-
theme: string | null;
|
|
34
|
-
impact: string | null;
|
|
35
|
-
help: string | null;
|
|
36
|
-
level: string | null;
|
|
37
|
-
example: string | null;
|
|
38
|
-
}
|
|
39
|
-
export interface MapResponse {
|
|
40
|
-
mappings: Record<string, RgaaMapping>;
|
|
41
|
-
coverage: {
|
|
42
|
-
mappedCriteria: number;
|
|
43
|
-
totalCriteria: number;
|
|
44
|
-
percentage: number;
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Détecte si une URL est locale (localhost, 127.0.0.1, 0.0.0.0)
|
|
49
|
-
*/
|
|
50
|
-
export declare function isLocalUrl(url: string): boolean;
|
|
51
|
-
/**
|
|
52
|
-
* Lance un scan axe-core local sur une URL
|
|
53
|
-
*
|
|
54
|
-
* @param url - URL locale à scanner (localhost, 127.0.0.1)
|
|
55
|
-
* @returns Violations axe-core brutes
|
|
56
|
-
*/
|
|
57
|
-
export declare function scanLocal(url: string): Promise<LocalScanResult>;
|
|
58
|
-
/**
|
|
59
|
-
* Appelle POST /api/ci/map pour enrichir les violations avec le mapping RGAA
|
|
60
|
-
*
|
|
61
|
-
* @param ruleIds - Liste de ruleIds axe-core
|
|
62
|
-
* @param apiUrl - URL de l'API RGAAudit
|
|
63
|
-
* @param apiKey - Clé API
|
|
64
|
-
* @returns Mappings RGAA ou null si l'API est inaccessible
|
|
65
|
-
*/
|
|
66
|
-
export declare function fetchRgaaMappings(ruleIds: string[], apiUrl: string, apiKey: string): Promise<MapResponse | null>;
|
package/dist/local-scanner.js
DELETED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Scanner local axe-core — Mode localhost
|
|
3
|
-
*
|
|
4
|
-
* Utilise puppeteer-core (Chrome existant) + @axe-core/puppeteer
|
|
5
|
-
* pour auditer des URLs localhost sans passer par l'API distante.
|
|
6
|
-
*
|
|
7
|
-
* Le mapping RGAA est ensuite récupéré via POST /api/ci/map.
|
|
8
|
-
*/
|
|
9
|
-
/**
|
|
10
|
-
* Détecte si une URL est locale (localhost, 127.0.0.1, 0.0.0.0)
|
|
11
|
-
*/
|
|
12
|
-
export function isLocalUrl(url) {
|
|
13
|
-
try {
|
|
14
|
-
const parsed = new URL(url);
|
|
15
|
-
return (parsed.hostname === 'localhost' ||
|
|
16
|
-
parsed.hostname === '127.0.0.1' ||
|
|
17
|
-
parsed.hostname === '0.0.0.0' ||
|
|
18
|
-
parsed.hostname === '::1');
|
|
19
|
-
}
|
|
20
|
-
catch {
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Trouve le chemin de Chrome/Chromium installé sur le système
|
|
26
|
-
*/
|
|
27
|
-
async function findChromePath() {
|
|
28
|
-
const { existsSync } = await import('fs');
|
|
29
|
-
const { join } = await import('path');
|
|
30
|
-
// Variable d'env explicite (priorité absolue, pas d'exécution shell)
|
|
31
|
-
if (process.env.CHROME_PATH && existsSync(process.env.CHROME_PATH)) {
|
|
32
|
-
return process.env.CHROME_PATH;
|
|
33
|
-
}
|
|
34
|
-
// Chemins connus par OS (pas d'exécution shell, pas d'injection possible)
|
|
35
|
-
const candidates = [];
|
|
36
|
-
// Windows — chemins standards via variables d'env système
|
|
37
|
-
const winDirs = [
|
|
38
|
-
process.env['PROGRAMFILES'],
|
|
39
|
-
process.env['PROGRAMFILES(X86)'],
|
|
40
|
-
process.env['LOCALAPPDATA'],
|
|
41
|
-
].filter(Boolean);
|
|
42
|
-
for (const dir of winDirs) {
|
|
43
|
-
candidates.push(join(dir, 'Google', 'Chrome', 'Application', 'chrome.exe'));
|
|
44
|
-
candidates.push(join(dir, 'Microsoft', 'Edge', 'Application', 'msedge.exe'));
|
|
45
|
-
}
|
|
46
|
-
// macOS
|
|
47
|
-
candidates.push('/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '/Applications/Chromium.app/Contents/MacOS/Chromium', '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge');
|
|
48
|
-
// Linux
|
|
49
|
-
candidates.push('/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium', '/usr/bin/chromium-browser', '/snap/bin/chromium');
|
|
50
|
-
for (const p of candidates) {
|
|
51
|
-
if (existsSync(p))
|
|
52
|
-
return p;
|
|
53
|
-
}
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Lance un scan axe-core local sur une URL
|
|
58
|
-
*
|
|
59
|
-
* @param url - URL locale à scanner (localhost, 127.0.0.1)
|
|
60
|
-
* @returns Violations axe-core brutes
|
|
61
|
-
*/
|
|
62
|
-
export async function scanLocal(url) {
|
|
63
|
-
// Validation URL : seuls http/https autorisés, longueur limitée
|
|
64
|
-
const parsed = new URL(url);
|
|
65
|
-
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
66
|
-
throw new Error('Seuls les protocoles http et https sont autorisés');
|
|
67
|
-
}
|
|
68
|
-
if (url.length > 2048) {
|
|
69
|
-
throw new Error('URL trop longue (max 2048 caractères)');
|
|
70
|
-
}
|
|
71
|
-
const chromePath = await findChromePath();
|
|
72
|
-
if (!chromePath) {
|
|
73
|
-
throw new Error('Chrome/Chromium non trouvé sur ce système.\n\n' +
|
|
74
|
-
'Le scan localhost nécessite un navigateur Chromium installé.\n' +
|
|
75
|
-
'Installez Google Chrome ou Chromium, puis réessayez.\n\n' +
|
|
76
|
-
'Chemins recherchés :\n' +
|
|
77
|
-
'- Windows : Program Files\\Google\\Chrome\\Application\\chrome.exe\n' +
|
|
78
|
-
'- macOS : /Applications/Google Chrome.app\n' +
|
|
79
|
-
'- Linux : /usr/bin/google-chrome ou /usr/bin/chromium');
|
|
80
|
-
}
|
|
81
|
-
// Import dynamique pour ne pas crasher si les deps ne sont pas installées
|
|
82
|
-
const puppeteer = await import('puppeteer-core');
|
|
83
|
-
const { AxePuppeteer } = await import('@axe-core/puppeteer');
|
|
84
|
-
let browser = null;
|
|
85
|
-
try {
|
|
86
|
-
browser = await puppeteer.default.launch({
|
|
87
|
-
executablePath: chromePath,
|
|
88
|
-
headless: true,
|
|
89
|
-
args: [
|
|
90
|
-
'--no-sandbox',
|
|
91
|
-
'--disable-setuid-sandbox',
|
|
92
|
-
'--disable-dev-shm-usage',
|
|
93
|
-
'--disable-gpu',
|
|
94
|
-
'--disable-extensions',
|
|
95
|
-
],
|
|
96
|
-
});
|
|
97
|
-
const page = await browser.newPage();
|
|
98
|
-
await page.setViewport({ width: 1280, height: 720 });
|
|
99
|
-
// Naviguer avec timeout
|
|
100
|
-
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30_000 });
|
|
101
|
-
// Lancer axe-core
|
|
102
|
-
const results = await new AxePuppeteer(page)
|
|
103
|
-
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'best-practice'])
|
|
104
|
-
.analyze();
|
|
105
|
-
const violations = results.violations.map(v => ({
|
|
106
|
-
ruleId: v.id,
|
|
107
|
-
impact: v.impact || 'moderate',
|
|
108
|
-
description: v.description,
|
|
109
|
-
help: v.help,
|
|
110
|
-
helpUrl: v.helpUrl,
|
|
111
|
-
nodesCount: v.nodes.length,
|
|
112
|
-
nodes: v.nodes.slice(0, 5).map(n => ({
|
|
113
|
-
html: (n.html || '').slice(0, 200),
|
|
114
|
-
target: Array.isArray(n.target) ? n.target.join(' > ') : String(n.target),
|
|
115
|
-
})),
|
|
116
|
-
}));
|
|
117
|
-
return {
|
|
118
|
-
url,
|
|
119
|
-
violations,
|
|
120
|
-
passes: results.passes.length,
|
|
121
|
-
incomplete: results.incomplete.length,
|
|
122
|
-
timestamp: new Date().toISOString(),
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
finally {
|
|
126
|
-
if (browser)
|
|
127
|
-
await browser.close().catch(() => { });
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Appelle POST /api/ci/map pour enrichir les violations avec le mapping RGAA
|
|
132
|
-
*
|
|
133
|
-
* @param ruleIds - Liste de ruleIds axe-core
|
|
134
|
-
* @param apiUrl - URL de l'API RGAAudit
|
|
135
|
-
* @param apiKey - Clé API
|
|
136
|
-
* @returns Mappings RGAA ou null si l'API est inaccessible
|
|
137
|
-
*/
|
|
138
|
-
export async function fetchRgaaMappings(ruleIds, apiUrl, apiKey) {
|
|
139
|
-
try {
|
|
140
|
-
const res = await fetch(`${apiUrl}/api/ci/map`, {
|
|
141
|
-
method: 'POST',
|
|
142
|
-
headers: {
|
|
143
|
-
'Content-Type': 'application/json',
|
|
144
|
-
'X-API-Key': apiKey,
|
|
145
|
-
},
|
|
146
|
-
body: JSON.stringify({ ruleIds }),
|
|
147
|
-
});
|
|
148
|
-
if (!res.ok)
|
|
149
|
-
return null;
|
|
150
|
-
return res.json();
|
|
151
|
-
}
|
|
152
|
-
catch {
|
|
153
|
-
// API inaccessible (offline, pas de connexion) → retour null
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
}
|