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.
@@ -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.0',
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 ({ url, parentProjectFolder, writeToProject, maxAssertions, scopeToRegions, analyzeStates, loginUsername, loginPassword, loginUrl, jiraNumber }) => {
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.0",
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
- data.testCode &&
59
- data.stepDefCode &&
60
- data.testCode.trim() === data.stepDefCode.trim();
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, data.stepDefCode || data.testCode || '', 'utf8');
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, data.stepDefCode, 'utf8');
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, data.testCode, 'utf8');
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, data.testCode, 'utf8');
83
+ await writeFile(abs, testStripped, 'utf8');
96
84
  written.push(relPath);
97
85
  }
98
86
  }