genat-mcp 2.3.1 → 2.3.3

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 CHANGED
@@ -225,8 +225,19 @@ Login credentials: provide in the prompt (loginUsername, loginPassword) or in pr
225
225
 
226
226
  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).
227
227
 
228
+ **Response fields for calling applications:** When `stepDefCode` is present, the MCP may set **`_stepDefIncomplete`** (boolean) and **`_stepDefIncompleteReason`** (string) using a lightweight heuristic (unclosed strings/delimiters, line continuation at EOF). Treat `true` as a signal to warn the user, retry generation, or block merge—do not assume the step file is runnable. Other optional fields from the workflow include **`usedLogin`** and **`loginReason`** when using login-capable workflows.
229
+
228
230
  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.
229
231
 
232
+ ### Feature ↔ step parity (consumer CI)
233
+
234
+ GenAT does not guarantee that every Gherkin line has an implemented step. For **pytest-bdd** projects, the calling application or CI can:
235
+
236
+ 1. Parse `*.feature` step lines (after `Given`/`When`/`Then`/`And`/`But`).
237
+ 2. Confirm `accessibility_steps.py` (or your step module) defines matching `@given`/`@when`/`@then` patterns (or run `pytest --collect-only` / a dry run).
238
+
239
+ Keep this as an optional pre-merge check alongside `_stepDefIncomplete`.
240
+
230
241
  ## Sample prompts
231
242
 
232
243
  See [prompts.md](prompts.md) for a full list of prompts including specific parent folders, login, JIRA, and combined options.
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Lightweight heuristic: detect likely truncated or syntactically incomplete pytest-bdd step Python.
3
+ * For calling apps to surface _stepDefIncomplete and retry or warn.
4
+ */
5
+
6
+ /**
7
+ * @param {string} code
8
+ * @returns {{ incomplete: boolean, reason: string | null }}
9
+ */
10
+ export function detectStepDefIncomplete(code) {
11
+ if (code == null || typeof code !== 'string') {
12
+ return { incomplete: false, reason: null };
13
+ }
14
+ const t = code.replace(/\r\n/g, '\n').trimEnd();
15
+ if (t.length === 0) {
16
+ return { incomplete: true, reason: 'empty' };
17
+ }
18
+
19
+ const triples = (t.match(/"""/g) || []).length;
20
+ if (triples % 2 !== 0) {
21
+ return { incomplete: true, reason: 'unclosed_triple_double_quoted_string' };
22
+ }
23
+ const tripleSingle = (t.match(/'''/g) || []).length;
24
+ if (tripleSingle % 2 !== 0) {
25
+ return { incomplete: true, reason: 'unclosed_triple_single_quoted_string' };
26
+ }
27
+
28
+ const lines = t.split('\n');
29
+ const lastLine = lines[lines.length - 1];
30
+ if (/\\\s*$/.test(lastLine)) {
31
+ return { incomplete: true, reason: 'line_continuation_at_eof' };
32
+ }
33
+
34
+ const depth = bracketDepthAtEnd(t);
35
+ if (depth !== 0) {
36
+ return { incomplete: true, reason: 'unclosed_delimiters' };
37
+ }
38
+
39
+ return { incomplete: false, reason: null };
40
+ }
41
+
42
+ /**
43
+ * Rough balance of (), [], {} at end of file, skipping strings and comments.
44
+ * @param {string} s
45
+ * @returns {number} 0 if balanced, non-zero if likely incomplete
46
+ */
47
+ function bracketDepthAtEnd(s) {
48
+ let i = 0;
49
+ const stack = [];
50
+ let state = 'code';
51
+
52
+ while (i < s.length) {
53
+ const c = s[i];
54
+
55
+ if (state === 'code') {
56
+ if (c === '#') {
57
+ while (i < s.length && s[i] !== '\n') i++;
58
+ continue;
59
+ }
60
+ if (s.slice(i, i + 3) === '"""') {
61
+ state = 'triple_double';
62
+ i += 3;
63
+ continue;
64
+ }
65
+ if (s.slice(i, i + 3) === "'''") {
66
+ state = 'triple_single';
67
+ i += 3;
68
+ continue;
69
+ }
70
+ if (c === '"' || c === "'") {
71
+ state = c === '"' ? 'string_double' : 'string_single';
72
+ i++;
73
+ continue;
74
+ }
75
+ if (c === '(') stack.push('(');
76
+ else if (c === '[') stack.push('[');
77
+ else if (c === '{') stack.push('{');
78
+ else if (c === ')' || c === ']' || c === '}') {
79
+ const want = c === ')' ? '(' : c === ']' ? '[' : '{';
80
+ if (stack.length === 0 || stack[stack.length - 1] !== want) {
81
+ return -1;
82
+ }
83
+ stack.pop();
84
+ }
85
+ i++;
86
+ continue;
87
+ }
88
+
89
+ if (state === 'string_single') {
90
+ if (c === '\\') {
91
+ i += 2;
92
+ continue;
93
+ }
94
+ if (c === "'") {
95
+ state = 'code';
96
+ }
97
+ i++;
98
+ continue;
99
+ }
100
+
101
+ if (state === 'string_double') {
102
+ if (c === '\\') {
103
+ i += 2;
104
+ continue;
105
+ }
106
+ if (c === '"') {
107
+ state = 'code';
108
+ }
109
+ i++;
110
+ continue;
111
+ }
112
+
113
+ if (state === 'triple_double') {
114
+ if (s.slice(i, i + 3) === '"""') {
115
+ state = 'code';
116
+ i += 3;
117
+ continue;
118
+ }
119
+ i++;
120
+ continue;
121
+ }
122
+
123
+ if (state === 'triple_single') {
124
+ if (s.slice(i, i + 3) === "'''") {
125
+ state = 'code';
126
+ i += 3;
127
+ continue;
128
+ }
129
+ i++;
130
+ continue;
131
+ }
132
+
133
+ i++;
134
+ }
135
+
136
+ if (state !== 'code') {
137
+ return 1;
138
+ }
139
+ return stack.length;
140
+ }
@@ -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,9 @@ 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 { detectStepDefIncomplete } from './detect-incomplete-step-def.js';
15
+ import { finalizePytestBddStepDef } from './finalize-pytest-bdd-steps.js';
16
+ import { resolvePytestBddPaths } from './pytest-bdd-paths.js';
14
17
  import { writeGeneratedFiles } from './write-files.js';
15
18
  import { loadOclcAccessibilityContext } from './oclc-context.js';
16
19
 
@@ -56,7 +59,7 @@ function insecureHttpsFetch(url, { method = 'GET', headers = {}, body, signal })
56
59
  const server = new McpServer(
57
60
  {
58
61
  name: 'GenAT',
59
- version: '2.3.1',
62
+ version: '2.3.3',
60
63
  },
61
64
  {
62
65
  capabilities: {
@@ -269,6 +272,24 @@ server.registerTool(
269
272
  };
270
273
  }
271
274
 
275
+ const bddResolved = (data?.bddFramework ?? framework.bddFramework ?? 'none').toString().toLowerCase();
276
+ if (framework.scriptType === 'python' && bddResolved === 'pytest-bdd' && resolvedPath) {
277
+ const pb = resolvePytestBddPaths(resolvedPath);
278
+ const sp = data.suggestedPaths || {};
279
+ const pathOverrides = {
280
+ featureRel: sp.feature || pb.featureRel,
281
+ stepRel: sp.stepDef || pb.stepRel,
282
+ };
283
+ if (data.stepDefCode) data.stepDefCode = finalizePytestBddStepDef(resolvedPath, data.stepDefCode, pathOverrides);
284
+ if (data.testCode) data.testCode = finalizePytestBddStepDef(resolvedPath, data.testCode, pathOverrides);
285
+ }
286
+
287
+ if (data.stepDefCode && typeof data.stepDefCode === 'string') {
288
+ const inc = detectStepDefIncomplete(data.stepDefCode);
289
+ data._stepDefIncomplete = inc.incomplete;
290
+ if (inc.reason) data._stepDefIncompleteReason = inc.reason;
291
+ }
292
+
272
293
  if (writeToProject && resolvedPath && data) {
273
294
  if (data.bddFramework == null) data.bddFramework = framework.bddFramework;
274
295
  try {
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "genat-mcp",
3
- "version": "2.3.1",
3
+ "version": "2.3.3",
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", "strip-gherkin-from-python-steps.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", "detect-incomplete-step-def.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/write-files.js CHANGED
@@ -2,34 +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';
8
- import { stripLeadingGherkinFromPythonSteps } from './strip-gherkin-from-python-steps.js';
7
+ import { finalizePytestBddStepDef } from './finalize-pytest-bdd-steps.js';
8
+ import { resolvePytestBddPaths } from './pytest-bdd-paths.js';
9
9
 
10
10
  const DEFAULT_DIR = 'tests/accessibility';
11
11
 
12
- /**
13
- * @param {string} root
14
- * @returns {{ featureRel: string, stepRel: string, specRel: string }}
15
- */
16
- function resolvePytestBddPaths(root) {
17
- let featureRel;
18
- if (existsSync(join(root, 'features'))) featureRel = join('features', 'accessibility.feature');
19
- else if (existsSync(join(root, 'tests', 'features'))) featureRel = join('tests', 'features', 'accessibility.feature');
20
- else featureRel = join('features', 'accessibility.feature');
21
-
22
- let stepRel;
23
- if (existsSync(join(root, 'step_defs'))) stepRel = join('step_defs', 'accessibility_steps.py');
24
- else if (existsSync(join(root, 'steps'))) stepRel = join('steps', 'accessibility_steps.py');
25
- else if (existsSync(join(root, 'tests', 'step_defs'))) stepRel = join('tests', 'step_defs', 'accessibility_steps.py');
26
- else if (existsSync(join(root, 'tests', 'steps'))) stepRel = join('tests', 'steps', 'accessibility_steps.py');
27
- else stepRel = join('step_defs', 'accessibility_steps.py');
28
-
29
- const specRel = join(DEFAULT_DIR, 'accessibility_test.py');
30
- return { featureRel, stepRel, specRel };
31
- }
32
-
33
12
  /**
34
13
  * @param {string} parentProjectFolder - project root path
35
14
  * @param {{ testCode?: string, scriptType?: string, bddFramework?: string, featureFile?: string, stepDefCode?: string, pageObjectCode?: string, suggestedPaths?: Record<string, string> }} data - workflow response
@@ -55,10 +34,14 @@ export async function writeGeneratedFiles(parentProjectFolder, data) {
55
34
 
56
35
  if (usePytestBddLayout) {
57
36
  const pb = resolvePytestBddPaths(root);
37
+ const pathOverrides = {
38
+ featureRel: suggested.feature || pb.featureRel,
39
+ stepRel: suggested.stepDef || pb.stepRel,
40
+ };
58
41
  const stepDefStripped = data.stepDefCode
59
- ? stripLeadingGherkinFromPythonSteps(data.stepDefCode)
42
+ ? finalizePytestBddStepDef(root, data.stepDefCode, pathOverrides)
60
43
  : '';
61
- const testStripped = data.testCode ? stripLeadingGherkinFromPythonSteps(data.testCode) : '';
44
+ const testStripped = data.testCode ? finalizePytestBddStepDef(root, data.testCode, pathOverrides) : '';
62
45
  const dup =
63
46
  testStripped &&
64
47
  stepDefStripped &&