genat-mcp 2.1.2 → 2.2.1
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 +83 -0
- package/index.js +52 -11
- package/package.json +1 -1
- package/run-genat.mjs +12 -0
package/README.md
CHANGED
|
@@ -35,6 +35,9 @@ npm install /path/to/n8n_playwright_tests/mcp-genat
|
|
|
35
35
|
| **N8N_WEBHOOK_URL** | No | `http://localhost:5678/webhook-test/webhook-genat-login` | n8n GenAT webhook URL. Set this if your n8n instance is elsewhere (e.g. `https://your-n8n-host/webhook-test/webhook-genat-login`). |
|
|
36
36
|
| **N8N_INSECURE_TLS** | No | (off) | Set to `1`, `true`, or `yes` to skip TLS certificate verification for HTTPS webhooks. Use only for dev/internal n8n with self-signed or internal CA certs (e.g. K8s). Not recommended for production. |
|
|
37
37
|
| **SERVICE_BASE_URL** | No | (none) | Base URL for DOM Analyzer and Accessibility Analyzer (e.g. `http://host.docker.internal` or `http://your-tunnel.ngrok.io`). Required when n8n runs on a remote host (K8s) so the workflow can reach the services. The workflow appends `:3456/analyze-dom` and `:3458/analyze-accessibility`. |
|
|
38
|
+
| **N8N_MAX_ASSERTIONS** | No | (none) | Override assertion count for complex pages (e.g. `25`). Used as default when the tool is invoked without `maxAssertions`; also used by run-genat.mjs. |
|
|
39
|
+
| **N8N_SCOPE_TO_REGIONS** | No | (none) | Comma-separated regions to focus tests on (e.g. `header,main,table`). Used as default when the tool is invoked without `scopeToRegions`; also used by run-genat.mjs. |
|
|
40
|
+
| **N8N_ANALYZE_STATES** | No | (off) | Set to `1`, `true`, or `yes` to analyze dropdown/combobox expanded states and include state-specific violations in generated tests. Used as default when the tool is invoked without `analyzeStates`; also used by run-genat.mjs. Increases analysis time. |
|
|
38
41
|
|
|
39
42
|
## Cursor MCP config
|
|
40
43
|
|
|
@@ -63,6 +66,45 @@ npm install /path/to/n8n_playwright_tests/mcp-genat
|
|
|
63
66
|
}
|
|
64
67
|
```
|
|
65
68
|
|
|
69
|
+
**With environment variables** (e.g. remote n8n, self-signed certs):
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"mcpServers": {
|
|
74
|
+
"GenAT": {
|
|
75
|
+
"command": "npx",
|
|
76
|
+
"args": ["genat-mcp"],
|
|
77
|
+
"env": {
|
|
78
|
+
"N8N_WEBHOOK_URL": "https://n8n.example.com/webhook-test/webhook-genat-login",
|
|
79
|
+
"N8N_INSECURE_TLS": "1",
|
|
80
|
+
"SERVICE_BASE_URL": "http://your-tunnel.ngrok.io"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**With optional params for complex pages** (`N8N_MAX_ASSERTIONS`, `N8N_SCOPE_TO_REGIONS`, `N8N_ANALYZE_STATES`):
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"mcpServers": {
|
|
92
|
+
"GenAT": {
|
|
93
|
+
"command": "npx",
|
|
94
|
+
"args": ["genat-mcp"],
|
|
95
|
+
"env": {
|
|
96
|
+
"N8N_WEBHOOK_URL": "https://n8n.example.com/webhook-test/webhook-genat-login",
|
|
97
|
+
"N8N_MAX_ASSERTIONS": "30",
|
|
98
|
+
"N8N_SCOPE_TO_REGIONS": "header,main,table",
|
|
99
|
+
"N8N_ANALYZE_STATES": "1"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
These env vars are passed to the webhook: `maxAssertions` (number), `scopeToRegions` (array), `analyzeStates` (boolean). Use `N8N_MAX_ASSERTIONS` for complex pages that need more assertions; `N8N_SCOPE_TO_REGIONS` to focus tests on specific regions; `N8N_ANALYZE_STATES` to run axe in expanded dropdown/combobox states (increases analysis time).
|
|
107
|
+
|
|
66
108
|
If the global binary is not on PATH, use the full path, e.g. `node $(npm root -g)/genat-mcp/index.js`.
|
|
67
109
|
|
|
68
110
|
## Requirements
|
|
@@ -83,6 +125,15 @@ N8N_WEBHOOK_URL='https://your-n8n/webhook-test/webhook-genat-login' N8N_INSECURE
|
|
|
83
125
|
N8N_WEBHOOK_URL='https://your-n8n/webhook-test/webhook-genat-login' N8N_INSECURE_TLS=1 node run-genat.mjs https://example.com /path/to/project
|
|
84
126
|
```
|
|
85
127
|
|
|
128
|
+
**Optional env vars** (same as MCP tool parameters):
|
|
129
|
+
- `N8N_MAX_ASSERTIONS` – Override assertion count (e.g. `25`)
|
|
130
|
+
- `N8N_SCOPE_TO_REGIONS` – Comma-separated regions (e.g. `header,main,table`)
|
|
131
|
+
- `N8N_ANALYZE_STATES` – Set to `1` to analyze expanded dropdown/combobox states
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
N8N_MAX_ASSERTIONS=30 N8N_SCOPE_TO_REGIONS=header,main N8N_ANALYZE_STATES=1 node node_modules/genat-mcp/run-genat.mjs https://example.com .
|
|
135
|
+
```
|
|
136
|
+
|
|
86
137
|
**Important:** Use `webhook-genat-login` (not `webhook-genat`). Import and activate [genat-accessibility-tests-with-login.json](../workflows/genat-accessibility-tests-with-login.json) in n8n.
|
|
87
138
|
|
|
88
139
|
**Optional – Cursor rule:** Copy the rule so the AI uses the correct webhook when creating workarounds (the MCP error responses also include this hint):
|
|
@@ -111,9 +162,41 @@ When creating run-genat.mjs: Use webhook-genat-login, NOT webhook-genat. Prefer
|
|
|
111
162
|
- **url** (string): Page URL to analyze for accessibility.
|
|
112
163
|
- **parentProjectFolder** (string): Path to the project root (TypeScript, JavaScript, or Python Playwright).
|
|
113
164
|
- **writeToProject** (boolean, optional): If `true`, write generated files under `tests/accessibility/` in the project folder.
|
|
165
|
+
- **maxAssertions** (number, optional): Override default assertion count for complex pages (e.g. `25`).
|
|
166
|
+
- **scopeToRegions** (string[], optional): Limit tests to specific regions (e.g. `["header", "main", "table"]`).
|
|
167
|
+
- **analyzeStates** (boolean, optional): If `true`, run axe in expanded dropdown/combobox states and include state-specific violations. Increases analysis time.
|
|
168
|
+
- **loginUsername** (string, optional): Login username. Overrides .env when provided. Requires loginPassword.
|
|
169
|
+
- **loginPassword** (string, optional): Login password. Overrides .env when provided. Requires loginUsername.
|
|
170
|
+
- **loginUrl** (string, optional): Login page URL if different from target URL.
|
|
171
|
+
|
|
172
|
+
Login credentials: provide in the prompt (loginUsername, loginPassword) or in project `.env` (TEST_USER, TEST_PASSWORD, LOGIN_USER, LOGIN_PASSWORD). Tool params override .env. For sensitive environments, prefer .env (gitignored) since credentials in the prompt are visible in chat history.
|
|
114
173
|
|
|
115
174
|
Framework detection: language (TypeScript vs JavaScript vs Python), BDD (Cucumber, pytest-bdd, Behave), and Page Object pattern (e.g. `pages/` directory or `*Page.ts`, `*Page.js`, `*_page.py` files). Returns JSON with `testCode`, `featureFile`, `stepDefCode`, `pageObjectCode` (and `_written` paths when writing).
|
|
116
175
|
|
|
176
|
+
Generated tests include keyboard accessibility checks (Tab order, focus) by default. For complex pages, the workflow uses page complexity (regions, tables, widgets) to scale assertion count.
|
|
177
|
+
|
|
178
|
+
## Sample prompts
|
|
179
|
+
|
|
180
|
+
**With login credentials in the prompt:**
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
Use GenAT to generate accessibility tests for https://myapp.example.com/dashboard. Use parentProjectFolder . and writeToProject true. Login with username myuser and password mypass.
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
Use GenAT with url https://myapp.example.com, loginUsername admin, loginPassword secret123, parentProjectFolder . Write the files to the project.
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**With credentials in .env:**
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
Use GenAT for https://myapp.example.com/dashboard. Credentials are in .env.
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
Use GenAT to generate accessibility tests for https://myapp.example.com/dashboard using this project folder: . Write the files to the project.
|
|
198
|
+
```
|
|
199
|
+
|
|
117
200
|
## Publishing to npm
|
|
118
201
|
|
|
119
202
|
Until the package is published, `npm install genat-mcp` returns 404. To publish:
|
package/index.js
CHANGED
|
@@ -51,7 +51,7 @@ function insecureHttpsFetch(url, { method = 'GET', headers = {}, body }) {
|
|
|
51
51
|
const server = new McpServer(
|
|
52
52
|
{
|
|
53
53
|
name: 'GenAT',
|
|
54
|
-
version: '2.1
|
|
54
|
+
version: '2.2.1',
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
57
|
capabilities: {
|
|
@@ -64,7 +64,7 @@ server.registerTool(
|
|
|
64
64
|
'GenAT',
|
|
65
65
|
{
|
|
66
66
|
description:
|
|
67
|
-
'Generate accessibility tests for a URL based on the project folder. Analyzes the page (DOM + a11y), detects project framework (TypeScript, JavaScript, or Python, BDD, Page Object), and returns generated test files (feature, spec/step defs, page objects). Optionally write files to the project folder. On failure:
|
|
67
|
+
'Generate accessibility tests for a URL based on the project folder. Analyzes the page (DOM + a11y), detects project framework (TypeScript, JavaScript, or Python, BDD, Page Object), and returns generated test files (feature, spec/step defs, page objects). Optionally write files to the project folder. On failure: run node node_modules/genat-mcp/run-genat.mjs (NOT run-genat.mjs in project root). Use webhook-genat-login in N8N_WEBHOOK_URL (never webhook-genat).',
|
|
68
68
|
inputSchema: {
|
|
69
69
|
url: z.string().describe('Page URL to analyze for accessibility (e.g. https://example.com)'),
|
|
70
70
|
parentProjectFolder: z
|
|
@@ -74,9 +74,33 @@ server.registerTool(
|
|
|
74
74
|
.boolean()
|
|
75
75
|
.optional()
|
|
76
76
|
.describe('If true, write generated files into the project folder (e.g. tests/accessibility/)'),
|
|
77
|
+
maxAssertions: z
|
|
78
|
+
.number()
|
|
79
|
+
.optional()
|
|
80
|
+
.describe('Override default assertion count (e.g. 25 for complex pages)'),
|
|
81
|
+
scopeToRegions: z
|
|
82
|
+
.array(z.string())
|
|
83
|
+
.optional()
|
|
84
|
+
.describe('Limit tests to specific regions (e.g. ["header", "main", "table"])'),
|
|
85
|
+
analyzeStates: z
|
|
86
|
+
.boolean()
|
|
87
|
+
.optional()
|
|
88
|
+
.describe('Enable interaction-state analysis (dropdowns, modals). Requires Phase 2.'),
|
|
89
|
+
loginUsername: z
|
|
90
|
+
.string()
|
|
91
|
+
.optional()
|
|
92
|
+
.describe('Login username (overrides .env when provided)'),
|
|
93
|
+
loginPassword: z
|
|
94
|
+
.string()
|
|
95
|
+
.optional()
|
|
96
|
+
.describe('Login password (overrides .env when provided)'),
|
|
97
|
+
loginUrl: z
|
|
98
|
+
.string()
|
|
99
|
+
.optional()
|
|
100
|
+
.describe('Login page URL if different from target URL'),
|
|
77
101
|
},
|
|
78
102
|
},
|
|
79
|
-
async ({ url, parentProjectFolder, writeToProject }) => {
|
|
103
|
+
async ({ url, parentProjectFolder, writeToProject, maxAssertions, scopeToRegions, analyzeStates, loginUsername, loginPassword, loginUrl }) => {
|
|
80
104
|
if (!url || typeof url !== 'string') {
|
|
81
105
|
return {
|
|
82
106
|
content: [{ type: 'text', text: JSON.stringify({ error: 'Missing or invalid "url"' }) }],
|
|
@@ -107,6 +131,16 @@ server.registerTool(
|
|
|
107
131
|
try {
|
|
108
132
|
login = detectLogin(parentProjectFolder || '.') || {};
|
|
109
133
|
} catch (_) {}
|
|
134
|
+
|
|
135
|
+
// Tool params override .env
|
|
136
|
+
if (loginUsername != null && loginUsername !== '') login.username = loginUsername;
|
|
137
|
+
if (loginPassword != null && loginPassword !== '') login.password = loginPassword;
|
|
138
|
+
if (loginUrl != null && loginUrl !== '') login.loginUrl = loginUrl;
|
|
139
|
+
|
|
140
|
+
const maxAssertionsResolved = maxAssertions ?? (process.env.N8N_MAX_ASSERTIONS ? parseInt(process.env.N8N_MAX_ASSERTIONS, 10) : null);
|
|
141
|
+
const scopeToRegionsResolved = scopeToRegions?.length ? scopeToRegions : (process.env.N8N_SCOPE_TO_REGIONS ? process.env.N8N_SCOPE_TO_REGIONS.split(',').map((s) => s.trim()).filter(Boolean) : null);
|
|
142
|
+
const analyzeStatesResolved = analyzeStates || /^1|true|yes$/i.test(process.env.N8N_ANALYZE_STATES || '');
|
|
143
|
+
|
|
110
144
|
const body = {
|
|
111
145
|
url,
|
|
112
146
|
scriptType: framework.scriptType,
|
|
@@ -114,12 +148,17 @@ server.registerTool(
|
|
|
114
148
|
pageObject: framework.pageObject,
|
|
115
149
|
projectSummary: framework.projectSummary,
|
|
116
150
|
...(SERVICE_BASE_URL && { serviceBaseUrl: SERVICE_BASE_URL }),
|
|
117
|
-
...(
|
|
118
|
-
...(
|
|
119
|
-
...(
|
|
120
|
-
...(login.
|
|
121
|
-
|
|
122
|
-
|
|
151
|
+
...(maxAssertionsResolved != null && !isNaN(maxAssertionsResolved) && { maxAssertions: maxAssertionsResolved }),
|
|
152
|
+
...(scopeToRegionsResolved?.length && { scopeToRegions: scopeToRegionsResolved }),
|
|
153
|
+
...(analyzeStatesResolved && { analyzeStates: true }),
|
|
154
|
+
...(login.username && login.password && {
|
|
155
|
+
loginUsername: login.username,
|
|
156
|
+
loginPassword: login.password,
|
|
157
|
+
...(login.loginUrl && { loginUrl: login.loginUrl }),
|
|
158
|
+
...(login.usernameSelector && { loginUsernameSelector: login.usernameSelector }),
|
|
159
|
+
...(login.passwordSelector && { loginPasswordSelector: login.passwordSelector }),
|
|
160
|
+
...(login.submitSelector && { loginSubmitSelector: login.submitSelector }),
|
|
161
|
+
}),
|
|
123
162
|
};
|
|
124
163
|
|
|
125
164
|
const useInsecureTls = N8N_INSECURE_TLS && N8N_WEBHOOK_URL.startsWith('https://');
|
|
@@ -134,6 +173,7 @@ server.registerTool(
|
|
|
134
173
|
? await insecureHttpsFetch(N8N_WEBHOOK_URL, fetchOpts)
|
|
135
174
|
: await fetch(N8N_WEBHOOK_URL, fetchOpts);
|
|
136
175
|
} catch (err) {
|
|
176
|
+
const loginUrl = N8N_WEBHOOK_URL.replace(/webhook-genat\/?$/, 'webhook-genat-login');
|
|
137
177
|
return {
|
|
138
178
|
content: [
|
|
139
179
|
{
|
|
@@ -142,7 +182,7 @@ server.registerTool(
|
|
|
142
182
|
error: 'Failed to call n8n webhook',
|
|
143
183
|
message: err instanceof Error ? err.message : String(err),
|
|
144
184
|
hint: 'Set N8N_WEBHOOK_URL to your n8n webhook URL. For self-signed/internal certs, set N8N_INSECURE_TLS=1',
|
|
145
|
-
workaround:
|
|
185
|
+
workaround: `Run: N8N_WEBHOOK_URL='${loginUrl}' N8N_INSECURE_TLS=1 node node_modules/genat-mcp/run-genat.mjs "<url>" . Use node_modules/genat-mcp/run-genat.mjs only (delete any run-genat.mjs in project root that uses webhook-genat).`,
|
|
146
186
|
}),
|
|
147
187
|
},
|
|
148
188
|
],
|
|
@@ -153,6 +193,7 @@ server.registerTool(
|
|
|
153
193
|
if (!response.ok) {
|
|
154
194
|
const text = await response.text();
|
|
155
195
|
const is404 = response.status === 404;
|
|
196
|
+
const loginUrl = N8N_WEBHOOK_URL.replace(/webhook-genat\/?$/, 'webhook-genat-login');
|
|
156
197
|
return {
|
|
157
198
|
content: [
|
|
158
199
|
{
|
|
@@ -161,7 +202,7 @@ server.registerTool(
|
|
|
161
202
|
error: `n8n returned ${response.status}`,
|
|
162
203
|
body: text.slice(0, 1000),
|
|
163
204
|
...(is404 && {
|
|
164
|
-
workaround:
|
|
205
|
+
workaround: `Run: N8N_WEBHOOK_URL='${loginUrl}' N8N_INSECURE_TLS=1 node node_modules/genat-mcp/run-genat.mjs "<url>" . Use node_modules/genat-mcp/run-genat.mjs only (delete any run-genat.mjs in project root that uses webhook-genat).`,
|
|
165
206
|
}),
|
|
166
207
|
}),
|
|
167
208
|
},
|
package/package.json
CHANGED
package/run-genat.mjs
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
* Env:
|
|
11
11
|
* N8N_WEBHOOK_URL - webhook URL (default: http://localhost:5678/webhook-test/webhook-genat-login)
|
|
12
12
|
* N8N_INSECURE_TLS - set to 1 for self-signed/internal certs
|
|
13
|
+
* N8N_MAX_ASSERTIONS - override assertion count (e.g. 25)
|
|
14
|
+
* N8N_SCOPE_TO_REGIONS - comma-separated regions (e.g. header,main,table)
|
|
15
|
+
* N8N_ANALYZE_STATES - set to 1 to analyze dropdown/combobox expanded states
|
|
13
16
|
*/
|
|
14
17
|
import https from 'node:https';
|
|
15
18
|
import { resolve } from 'node:path';
|
|
@@ -71,12 +74,21 @@ async function main() {
|
|
|
71
74
|
login = detectLogin(parentProjectFolder) || {};
|
|
72
75
|
} catch (_) {}
|
|
73
76
|
|
|
77
|
+
const maxAssertions = process.env.N8N_MAX_ASSERTIONS ? parseInt(process.env.N8N_MAX_ASSERTIONS, 10) : null;
|
|
78
|
+
const scopeToRegions = process.env.N8N_SCOPE_TO_REGIONS
|
|
79
|
+
? process.env.N8N_SCOPE_TO_REGIONS.split(',').map((s) => s.trim()).filter(Boolean)
|
|
80
|
+
: null;
|
|
81
|
+
const analyzeStates = /^1|true|yes$/i.test(process.env.N8N_ANALYZE_STATES || '');
|
|
82
|
+
|
|
74
83
|
const body = {
|
|
75
84
|
url,
|
|
76
85
|
scriptType: framework.scriptType,
|
|
77
86
|
bddFramework: framework.bddFramework,
|
|
78
87
|
pageObject: framework.pageObject,
|
|
79
88
|
projectSummary: framework.projectSummary,
|
|
89
|
+
...(maxAssertions != null && !isNaN(maxAssertions) && { maxAssertions }),
|
|
90
|
+
...(scopeToRegions?.length && { scopeToRegions }),
|
|
91
|
+
...(analyzeStates && { analyzeStates: true }),
|
|
80
92
|
...(login.username && { loginUsername: login.username }),
|
|
81
93
|
...(login.password && { loginPassword: login.password }),
|
|
82
94
|
...(login.loginUrl && { loginUrl: login.loginUrl }),
|