genat-mcp 2.3.0 → 2.3.2
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/finalize-pytest-bdd-steps.js +22 -0
- package/fix-pytest-bdd-scenario-path.js +28 -0
- package/index.js +43 -2
- package/package.json +2 -2
- package/pytest-bdd-paths.js +28 -0
- package/run-genat.mjs +5 -0
- package/strip-gherkin-from-python-steps.js +30 -0
- package/write-files.js +17 -29
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip accidental Gherkin prefix and rewrite scenario() paths for pytest-bdd step modules.
|
|
3
|
+
*/
|
|
4
|
+
import { rewriteScenarioFeaturePaths } from './fix-pytest-bdd-scenario-path.js';
|
|
5
|
+
import { resolvePytestBddPaths } from './pytest-bdd-paths.js';
|
|
6
|
+
import { stripLeadingGherkinFromPythonSteps } from './strip-gherkin-from-python-steps.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} root - project root
|
|
10
|
+
* @param {string} stepDefCode
|
|
11
|
+
* @param {{ featureRel?: string, stepRel?: string }} [paths] - optional overrides (e.g. from suggestedPaths) for where feature and step files are written
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
export function finalizePytestBddStepDef(root, stepDefCode, paths) {
|
|
15
|
+
if (!stepDefCode || typeof stepDefCode !== 'string') return stepDefCode;
|
|
16
|
+
const pb = resolvePytestBddPaths(root);
|
|
17
|
+
const featureRel = paths?.featureRel ?? pb.featureRel;
|
|
18
|
+
const stepRel = paths?.stepRel ?? pb.stepRel;
|
|
19
|
+
let s = stripLeadingGherkinFromPythonSteps(stepDefCode);
|
|
20
|
+
s = rewriteScenarioFeaturePaths(s, root, featureRel, stepRel);
|
|
21
|
+
return s;
|
|
22
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rewrite pytest-bdd scenario() / @scenario() first argument so the .feature path is correct
|
|
3
|
+
* relative to the step file directory (matches where write-files places the feature).
|
|
4
|
+
*/
|
|
5
|
+
import { dirname, join, relative, resolve } from 'path';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} code
|
|
9
|
+
* @param {string} root - project root (absolute or resolved)
|
|
10
|
+
* @param {string} featureRel - path to .feature relative to project root (posix-style segments)
|
|
11
|
+
* @param {string} stepRel - path to step .py relative to project root
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
export function rewriteScenarioFeaturePaths(code, root, featureRel, stepRel) {
|
|
15
|
+
if (!code || typeof code !== 'string') return code;
|
|
16
|
+
const rootAbs = resolve(root);
|
|
17
|
+
const featureAbs = join(rootAbs, featureRel);
|
|
18
|
+
const stepDir = dirname(join(rootAbs, stepRel));
|
|
19
|
+
let posixRel = relative(stepDir, featureAbs);
|
|
20
|
+
if (!posixRel.startsWith('.')) {
|
|
21
|
+
posixRel = posixRel ? `./${posixRel}` : '.';
|
|
22
|
+
}
|
|
23
|
+
posixRel = posixRel.replace(/\\/g, '/');
|
|
24
|
+
|
|
25
|
+
return code.replace(/(@scenario|scenario)\s*\(\s*(['"])([^'"]*\.feature)\2/gi, (_m, decorator, quote) => {
|
|
26
|
+
return `${decorator}(${quote}${posixRel}${quote}`;
|
|
27
|
+
});
|
|
28
|
+
}
|
package/index.js
CHANGED
|
@@ -11,6 +11,8 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
11
11
|
import { z } from 'zod';
|
|
12
12
|
import { detectFramework } from './detect-framework.js';
|
|
13
13
|
import { detectLogin } from './detect-login.js';
|
|
14
|
+
import { finalizePytestBddStepDef } from './finalize-pytest-bdd-steps.js';
|
|
15
|
+
import { resolvePytestBddPaths } from './pytest-bdd-paths.js';
|
|
14
16
|
import { writeGeneratedFiles } from './write-files.js';
|
|
15
17
|
import { loadOclcAccessibilityContext } from './oclc-context.js';
|
|
16
18
|
|
|
@@ -56,7 +58,7 @@ function insecureHttpsFetch(url, { method = 'GET', headers = {}, body, signal })
|
|
|
56
58
|
const server = new McpServer(
|
|
57
59
|
{
|
|
58
60
|
name: 'GenAT',
|
|
59
|
-
version: '2.3.
|
|
61
|
+
version: '2.3.2',
|
|
60
62
|
},
|
|
61
63
|
{
|
|
62
64
|
capabilities: {
|
|
@@ -103,13 +105,38 @@ server.registerTool(
|
|
|
103
105
|
.string()
|
|
104
106
|
.optional()
|
|
105
107
|
.describe('Login page URL if different from target URL'),
|
|
108
|
+
requiresAuth: z
|
|
109
|
+
.boolean()
|
|
110
|
+
.optional()
|
|
111
|
+
.describe(
|
|
112
|
+
'v2 workflow only: if true, after the first unauthenticated fetch, retry with login when credentials exist (protected pages when DOM/URL heuristics miss). Omit on public pages.'
|
|
113
|
+
),
|
|
114
|
+
forceLogin: z
|
|
115
|
+
.boolean()
|
|
116
|
+
.optional()
|
|
117
|
+
.describe(
|
|
118
|
+
'v2 workflow only: same behavior as requiresAuth when true (optional alternate flag for tooling).'
|
|
119
|
+
),
|
|
106
120
|
jiraNumber: z
|
|
107
121
|
.string()
|
|
108
122
|
.optional()
|
|
109
123
|
.describe('JIRA issue key (e.g. PROJ-123, RB-40039). Extract from prompts like "JIRA RB-40039" or "JIRA ticket PROJ-123". Pass only the key (e.g. RB-40039), not "JIRA RB-40039".'),
|
|
110
124
|
},
|
|
111
125
|
},
|
|
112
|
-
async ({
|
|
126
|
+
async ({
|
|
127
|
+
url,
|
|
128
|
+
parentProjectFolder,
|
|
129
|
+
writeToProject,
|
|
130
|
+
maxAssertions,
|
|
131
|
+
scopeToRegions,
|
|
132
|
+
analyzeStates,
|
|
133
|
+
loginUsername,
|
|
134
|
+
loginPassword,
|
|
135
|
+
loginUrl,
|
|
136
|
+
requiresAuth,
|
|
137
|
+
forceLogin,
|
|
138
|
+
jiraNumber,
|
|
139
|
+
}) => {
|
|
113
140
|
if (!url || typeof url !== 'string') {
|
|
114
141
|
return {
|
|
115
142
|
content: [{ type: 'text', text: JSON.stringify({ error: 'Missing or invalid "url"' }) }],
|
|
@@ -174,6 +201,8 @@ server.registerTool(
|
|
|
174
201
|
...(login.passwordSelector && { loginPasswordSelector: login.passwordSelector }),
|
|
175
202
|
...(login.submitSelector && { loginSubmitSelector: login.submitSelector }),
|
|
176
203
|
}),
|
|
204
|
+
...(requiresAuth === true && { requiresAuth: true }),
|
|
205
|
+
...(forceLogin === true && { forceLogin: true }),
|
|
177
206
|
...(jiraNumber && jiraNumber.trim() && { jiraNumber: jiraNumber.trim() }),
|
|
178
207
|
};
|
|
179
208
|
|
|
@@ -242,6 +271,18 @@ server.registerTool(
|
|
|
242
271
|
};
|
|
243
272
|
}
|
|
244
273
|
|
|
274
|
+
const bddResolved = (data?.bddFramework ?? framework.bddFramework ?? 'none').toString().toLowerCase();
|
|
275
|
+
if (framework.scriptType === 'python' && bddResolved === 'pytest-bdd' && resolvedPath) {
|
|
276
|
+
const pb = resolvePytestBddPaths(resolvedPath);
|
|
277
|
+
const sp = data.suggestedPaths || {};
|
|
278
|
+
const pathOverrides = {
|
|
279
|
+
featureRel: sp.feature || pb.featureRel,
|
|
280
|
+
stepRel: sp.stepDef || pb.stepRel,
|
|
281
|
+
};
|
|
282
|
+
if (data.stepDefCode) data.stepDefCode = finalizePytestBddStepDef(resolvedPath, data.stepDefCode, pathOverrides);
|
|
283
|
+
if (data.testCode) data.testCode = finalizePytestBddStepDef(resolvedPath, data.testCode, pathOverrides);
|
|
284
|
+
}
|
|
285
|
+
|
|
245
286
|
if (writeToProject && resolvedPath && data) {
|
|
246
287
|
if (data.bddFramework == null) data.bddFramework = framework.bddFramework;
|
|
247
288
|
try {
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genat-mcp",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.2",
|
|
4
4
|
"mcpName": "io.github.asokans@oclc.org/genat",
|
|
5
5
|
"description": "MCP server GenAT: generate accessibility tests via n8n workflow (url + project folder)",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "index.js",
|
|
8
8
|
"bin": { "genat-mcp": "index.js" },
|
|
9
9
|
"engines": { "node": ">=20" },
|
|
10
|
-
"files": ["index.js", "detect-framework.js", "detect-login.js", "write-files.js", "oclc-context.js", "oclc-accessibility-confluence-context.md", "run-genat.mjs", "rules/genat-mcp.mdc"],
|
|
10
|
+
"files": ["index.js", "detect-framework.js", "detect-login.js", "write-files.js", "strip-gherkin-from-python-steps.js", "finalize-pytest-bdd-steps.js", "fix-pytest-bdd-scenario-path.js", "pytest-bdd-paths.js", "oclc-context.js", "oclc-accessibility-confluence-context.md", "run-genat.mjs", "rules/genat-mcp.mdc"],
|
|
11
11
|
"keywords": ["mcp", "accessibility", "playwright", "n8n", "model-context-protocol", "a11y", "testing"],
|
|
12
12
|
"repository": {
|
|
13
13
|
"type": "git",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve pytest-bdd feature / step / spec paths from project layout (same rules as write-files).
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_DIR = 'tests/accessibility';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} root
|
|
11
|
+
* @returns {{ featureRel: string, stepRel: string, specRel: string }}
|
|
12
|
+
*/
|
|
13
|
+
export function resolvePytestBddPaths(root) {
|
|
14
|
+
let featureRel;
|
|
15
|
+
if (existsSync(join(root, 'features'))) featureRel = join('features', 'accessibility.feature');
|
|
16
|
+
else if (existsSync(join(root, 'tests', 'features'))) featureRel = join('tests', 'features', 'accessibility.feature');
|
|
17
|
+
else featureRel = join('features', 'accessibility.feature');
|
|
18
|
+
|
|
19
|
+
let stepRel;
|
|
20
|
+
if (existsSync(join(root, 'step_defs'))) stepRel = join('step_defs', 'accessibility_steps.py');
|
|
21
|
+
else if (existsSync(join(root, 'steps'))) stepRel = join('steps', 'accessibility_steps.py');
|
|
22
|
+
else if (existsSync(join(root, 'tests', 'step_defs'))) stepRel = join('tests', 'step_defs', 'accessibility_steps.py');
|
|
23
|
+
else if (existsSync(join(root, 'tests', 'steps'))) stepRel = join('tests', 'steps', 'accessibility_steps.py');
|
|
24
|
+
else stepRel = join('step_defs', 'accessibility_steps.py');
|
|
25
|
+
|
|
26
|
+
const specRel = join(DEFAULT_DIR, 'accessibility_test.py');
|
|
27
|
+
return { featureRel, stepRel, specRel };
|
|
28
|
+
}
|
package/run-genat.mjs
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* N8N_SCOPE_TO_REGIONS - comma-separated regions (e.g. header,main,table)
|
|
15
15
|
* N8N_ANALYZE_STATES - set to 1 to analyze dropdown/combobox expanded states
|
|
16
16
|
* GENAT_OCLC_ACCESSIBILITY_CONTEXT - optional; overrides bundled oclc-accessibility-confluence-context.md for the workflow prompt
|
|
17
|
+
* GENAT_REQUIRES_AUTH / GENAT_FORCE_LOGIN - set to 1/true for v2 workflow: request login retry when credentials exist (after first unauthenticated fetch)
|
|
17
18
|
*/
|
|
18
19
|
import https from 'node:https';
|
|
19
20
|
import { resolve } from 'node:path';
|
|
@@ -87,6 +88,8 @@ async function main() {
|
|
|
87
88
|
? process.env.N8N_SCOPE_TO_REGIONS.split(',').map((s) => s.trim()).filter(Boolean)
|
|
88
89
|
: null;
|
|
89
90
|
const analyzeStates = /^1|true|yes$/i.test(process.env.N8N_ANALYZE_STATES || '');
|
|
91
|
+
const requiresAuth = /^1|true|yes$/i.test(process.env.GENAT_REQUIRES_AUTH || '');
|
|
92
|
+
const forceLogin = /^1|true|yes$/i.test(process.env.GENAT_FORCE_LOGIN || '');
|
|
90
93
|
const oclcAccessibilityContext = loadOclcAccessibilityContext();
|
|
91
94
|
|
|
92
95
|
const body = {
|
|
@@ -106,6 +109,8 @@ async function main() {
|
|
|
106
109
|
...(login.usernameSelector && { loginUsernameSelector: login.usernameSelector }),
|
|
107
110
|
...(login.passwordSelector && { loginPasswordSelector: login.passwordSelector }),
|
|
108
111
|
...(login.submitSelector && { loginSubmitSelector: login.submitSelector }),
|
|
112
|
+
...(requiresAuth && { requiresAuth: true }),
|
|
113
|
+
...(forceLogin && { forceLogin: true }),
|
|
109
114
|
};
|
|
110
115
|
|
|
111
116
|
const fetchOpts = {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remove accidental leading Gherkin (.feature text) from pytest-bdd step file content
|
|
3
|
+
* when the model duplicates the feature inside ```steps```.
|
|
4
|
+
* @param {string} s
|
|
5
|
+
* @returns {string}
|
|
6
|
+
*/
|
|
7
|
+
export function stripLeadingGherkinFromPythonSteps(s) {
|
|
8
|
+
if (!s || typeof s !== 'string') return s;
|
|
9
|
+
const lines = s.split('\n');
|
|
10
|
+
const firstImport = lines.findIndex((l) => /^\s*(import |from )/.test(l));
|
|
11
|
+
if (firstImport > 0) {
|
|
12
|
+
const head = lines.slice(0, firstImport).join('\n');
|
|
13
|
+
if (
|
|
14
|
+
/Feature\s*:|Scenario(\s+Outline)?\s*:|^\s*(Given|When|Then|And|But)\s*:|^\s*Background\s*:/im.test(head)
|
|
15
|
+
) {
|
|
16
|
+
return lines.slice(firstImport).join('\n').trim();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const firstStep = lines.findIndex((l) => {
|
|
20
|
+
const t = l.trim();
|
|
21
|
+
return /^@(given|when|then|step|pytest|fixture)/i.test(t) || /^def /.test(t);
|
|
22
|
+
});
|
|
23
|
+
if (firstStep > 0) {
|
|
24
|
+
const head = lines.slice(0, firstStep).join('\n');
|
|
25
|
+
if (/Feature\s*:|Scenario(\s+Outline)?\s*:/im.test(head)) {
|
|
26
|
+
return lines.slice(firstStep).join('\n').trim();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return s.trim();
|
|
30
|
+
}
|
package/write-files.js
CHANGED
|
@@ -2,33 +2,13 @@
|
|
|
2
2
|
* Write generated accessibility test files into the project folder.
|
|
3
3
|
* Uses suggested paths from the workflow response when present.
|
|
4
4
|
*/
|
|
5
|
-
import { existsSync } from 'fs';
|
|
6
5
|
import { writeFile, mkdir } from 'fs/promises';
|
|
7
6
|
import { dirname, join, resolve } from 'path';
|
|
7
|
+
import { finalizePytestBddStepDef } from './finalize-pytest-bdd-steps.js';
|
|
8
|
+
import { resolvePytestBddPaths } from './pytest-bdd-paths.js';
|
|
8
9
|
|
|
9
10
|
const DEFAULT_DIR = 'tests/accessibility';
|
|
10
11
|
|
|
11
|
-
/**
|
|
12
|
-
* @param {string} root
|
|
13
|
-
* @returns {{ featureRel: string, stepRel: string, specRel: string }}
|
|
14
|
-
*/
|
|
15
|
-
function resolvePytestBddPaths(root) {
|
|
16
|
-
let featureRel;
|
|
17
|
-
if (existsSync(join(root, 'features'))) featureRel = join('features', 'accessibility.feature');
|
|
18
|
-
else if (existsSync(join(root, 'tests', 'features'))) featureRel = join('tests', 'features', 'accessibility.feature');
|
|
19
|
-
else featureRel = join('features', 'accessibility.feature');
|
|
20
|
-
|
|
21
|
-
let stepRel;
|
|
22
|
-
if (existsSync(join(root, 'step_defs'))) stepRel = join('step_defs', 'accessibility_steps.py');
|
|
23
|
-
else if (existsSync(join(root, 'steps'))) stepRel = join('steps', 'accessibility_steps.py');
|
|
24
|
-
else if (existsSync(join(root, 'tests', 'step_defs'))) stepRel = join('tests', 'step_defs', 'accessibility_steps.py');
|
|
25
|
-
else if (existsSync(join(root, 'tests', 'steps'))) stepRel = join('tests', 'steps', 'accessibility_steps.py');
|
|
26
|
-
else stepRel = join('step_defs', 'accessibility_steps.py');
|
|
27
|
-
|
|
28
|
-
const specRel = join(DEFAULT_DIR, 'accessibility_test.py');
|
|
29
|
-
return { featureRel, stepRel, specRel };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
12
|
/**
|
|
33
13
|
* @param {string} parentProjectFolder - project root path
|
|
34
14
|
* @param {{ testCode?: string, scriptType?: string, bddFramework?: string, featureFile?: string, stepDefCode?: string, pageObjectCode?: string, suggestedPaths?: Record<string, string> }} data - workflow response
|
|
@@ -54,10 +34,18 @@ export async function writeGeneratedFiles(parentProjectFolder, data) {
|
|
|
54
34
|
|
|
55
35
|
if (usePytestBddLayout) {
|
|
56
36
|
const pb = resolvePytestBddPaths(root);
|
|
37
|
+
const pathOverrides = {
|
|
38
|
+
featureRel: suggested.feature || pb.featureRel,
|
|
39
|
+
stepRel: suggested.stepDef || pb.stepRel,
|
|
40
|
+
};
|
|
41
|
+
const stepDefStripped = data.stepDefCode
|
|
42
|
+
? finalizePytestBddStepDef(root, data.stepDefCode, pathOverrides)
|
|
43
|
+
: '';
|
|
44
|
+
const testStripped = data.testCode ? finalizePytestBddStepDef(root, data.testCode, pathOverrides) : '';
|
|
57
45
|
const dup =
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
46
|
+
testStripped &&
|
|
47
|
+
stepDefStripped &&
|
|
48
|
+
testStripped.trim() === stepDefStripped.trim();
|
|
61
49
|
|
|
62
50
|
if (data.featureFile) {
|
|
63
51
|
const relPath = suggested.feature || pb.featureRel;
|
|
@@ -71,14 +59,14 @@ export async function writeGeneratedFiles(parentProjectFolder, data) {
|
|
|
71
59
|
const relPath = suggested.stepDef || pb.stepRel;
|
|
72
60
|
const abs = join(root, relPath);
|
|
73
61
|
await mkdir(dirname(abs), { recursive: true });
|
|
74
|
-
await writeFile(abs,
|
|
62
|
+
await writeFile(abs, stepDefStripped || testStripped || '', 'utf8');
|
|
75
63
|
written.push(relPath);
|
|
76
64
|
} else {
|
|
77
65
|
if (data.stepDefCode) {
|
|
78
66
|
const relPath = suggested.stepDef || pb.stepRel;
|
|
79
67
|
const abs = join(root, relPath);
|
|
80
68
|
await mkdir(dirname(abs), { recursive: true });
|
|
81
|
-
await writeFile(abs,
|
|
69
|
+
await writeFile(abs, stepDefStripped, 'utf8');
|
|
82
70
|
written.push(relPath);
|
|
83
71
|
}
|
|
84
72
|
if (data.testCode) {
|
|
@@ -86,13 +74,13 @@ export async function writeGeneratedFiles(parentProjectFolder, data) {
|
|
|
86
74
|
const relPath = suggested.spec || pb.specRel;
|
|
87
75
|
const abs = join(root, relPath);
|
|
88
76
|
await mkdir(dirname(abs), { recursive: true });
|
|
89
|
-
await writeFile(abs,
|
|
77
|
+
await writeFile(abs, testStripped, 'utf8');
|
|
90
78
|
written.push(relPath);
|
|
91
79
|
} else {
|
|
92
80
|
const relPath = suggested.stepDef || pb.stepRel;
|
|
93
81
|
const abs = join(root, relPath);
|
|
94
82
|
await mkdir(dirname(abs), { recursive: true });
|
|
95
|
-
await writeFile(abs,
|
|
83
|
+
await writeFile(abs, testStripped, 'utf8');
|
|
96
84
|
written.push(relPath);
|
|
97
85
|
}
|
|
98
86
|
}
|