genat-mcp 2.2.3 → 2.2.5

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.
@@ -18,7 +18,7 @@ export function detectFramework(parentProjectFolder) {
18
18
  scriptType: 'typescript',
19
19
  bddFramework: 'none',
20
20
  pageObject: false,
21
- projectSummary: 'Folder not found or empty; using defaults.',
21
+ projectSummary: `Folder not found or empty; using defaults. (resolved: ${root})`,
22
22
  };
23
23
  }
24
24
 
@@ -44,8 +44,18 @@ export function detectFramework(parentProjectFolder) {
44
44
  const hasTs = hasExtension(root, '.ts') || hasExtension(root, '.spec.ts');
45
45
  const hasJs = hasExtension(root, '.js') || hasExtension(root, '.spec.js') || hasExtension(root, '.jsx') || hasExtension(root, '.spec.jsx');
46
46
  const hasPy = hasExtension(root, '.py') || hasExtension(root, '_test.py');
47
+ const hasConftest = walk(root, (f) => f === 'conftest.py').length > 0;
47
48
 
48
- if (hasPy && (hasRequirements || hasPyproject || hasPy)) {
49
+ // When both package.json and pyproject.toml exist, prefer Python if pyproject indicates Python project
50
+ let pyprojectIndicatesPython = false;
51
+ if (hasPyproject) {
52
+ try {
53
+ const pyproject = readFileSync(join(root, 'pyproject.toml'), 'utf8');
54
+ pyprojectIndicatesPython = /\[project\]/.test(pyproject) && (/pytest/i.test(pyproject) || /pytest-bdd/i.test(pyproject));
55
+ } catch (_) {}
56
+ }
57
+
58
+ if ((hasPy || hasConftest || pyprojectIndicatesPython) && (hasRequirements || hasPyproject || hasPy || hasConftest)) {
49
59
  scriptType = 'python';
50
60
  summaryParts.push('Language: Python');
51
61
  } else if (hasPackageJson || hasTsConfig || hasTs || hasJs) {
@@ -85,9 +95,38 @@ export function detectFramework(parentProjectFolder) {
85
95
  summaryParts.push('BDD: Behave');
86
96
  }
87
97
  }
98
+ if (bddFramework === 'none' && hasPyproject) {
99
+ try {
100
+ const pyproject = readFileSync(join(root, 'pyproject.toml'), 'utf8');
101
+ if (/pytest-bdd/i.test(pyproject)) {
102
+ bddFramework = 'pytest-bdd';
103
+ summaryParts.push('BDD: pytest-bdd (pyproject.toml)');
104
+ } else if (/behave/i.test(pyproject)) {
105
+ bddFramework = 'behave';
106
+ summaryParts.push('BDD: Behave (pyproject.toml)');
107
+ }
108
+ } catch (_) {}
109
+ }
88
110
  if (bddFramework === 'none' && existsSync(join(root, 'features'))) {
89
- bddFramework = 'behave';
90
- summaryParts.push('BDD: Behave (features/ present)');
111
+ // features/ exists: check .py files for pytest_bdd before defaulting to behave
112
+ const pyFilePaths = walkWithPaths(root, (f) => f.endsWith('.py'));
113
+ let foundPytestBdd = false;
114
+ for (const full of pyFilePaths) {
115
+ try {
116
+ const content = readFileSync(full, 'utf8');
117
+ if (/pytest_bdd|pytest-bdd|from pytest_bdd|import pytest_bdd/i.test(content)) {
118
+ foundPytestBdd = true;
119
+ break;
120
+ }
121
+ } catch (_) {}
122
+ }
123
+ if (foundPytestBdd) {
124
+ bddFramework = 'pytest-bdd';
125
+ summaryParts.push('BDD: pytest-bdd (found in .py files)');
126
+ } else {
127
+ bddFramework = 'behave';
128
+ summaryParts.push('BDD: Behave (features/ present)');
129
+ }
91
130
  }
92
131
  if (bddFramework === 'none' && hasFeatureFiles) summaryParts.push('Found .feature files');
93
132
  } catch (_) {}
@@ -96,15 +135,33 @@ export function detectFramework(parentProjectFolder) {
96
135
  summaryParts.push('Found .feature files (consider Cucumber)');
97
136
  }
98
137
 
99
- // Page Object: pages/ or *Page.ts, *Page.js, *_page.py
138
+ // Page Object: pages/, page_objects/, or *Page.ts, *Page.js, *_page.py, *page.py
100
139
  try {
140
+ const hasPagesInSubdir = (function findPagesDir(dir) {
141
+ try {
142
+ const entries = readdirSync(dir, { withFileTypes: true });
143
+ for (const e of entries) {
144
+ if (e.isDirectory() && (e.name === 'pages' || e.name === 'page_objects')) return true;
145
+ if (e.isDirectory() && !['node_modules', '.git', '__pycache__', '.venv', 'venv'].includes(e.name)) {
146
+ if (findPagesDir(join(dir, e.name))) return true;
147
+ }
148
+ }
149
+ } catch (_) {}
150
+ return false;
151
+ })(root);
101
152
  if (existsSync(join(root, 'pages'))) {
102
153
  pageObject = true;
103
154
  summaryParts.push('Page Object: pages/ directory');
155
+ } else if (existsSync(join(root, 'page_objects'))) {
156
+ pageObject = true;
157
+ summaryParts.push('Page Object: page_objects/ directory');
158
+ } else if (hasPagesInSubdir) {
159
+ pageObject = true;
160
+ summaryParts.push('Page Object: pages/ or page_objects/ in subdir');
104
161
  } else {
105
162
  const pageTs = walk(root, (f) => f.endsWith('Page.ts') || f.endsWith('Page.tsx'));
106
163
  const pageJs = walk(root, (f) => f.endsWith('Page.js') || f.endsWith('Page.jsx'));
107
- const pagePy = walk(root, (f) => f.endsWith('_page.py') || f.endsWith('Page.py'));
164
+ const pagePy = walk(root, (f) => f.endsWith('_page.py') || f.endsWith('Page.py') || f.endsWith('page.py'));
108
165
  if (pageTs.length || pageJs.length || pagePy.length) {
109
166
  pageObject = true;
110
167
  summaryParts.push('Page Object: *Page.ts / *Page.js / *_page.py files');
@@ -112,7 +169,10 @@ export function detectFramework(parentProjectFolder) {
112
169
  }
113
170
  } catch (_) {}
114
171
 
115
- const projectSummary = summaryParts.length ? summaryParts.join('; ') : 'No framework hints detected.';
172
+ const projectSummary = [
173
+ ...(summaryParts.length ? summaryParts : ['No framework hints detected']),
174
+ `(scanned: ${root})`,
175
+ ].join('; ');
116
176
 
117
177
  return {
118
178
  scriptType,
@@ -122,6 +182,30 @@ export function detectFramework(parentProjectFolder) {
122
182
  };
123
183
  }
124
184
 
185
+ /**
186
+ * Walk directory and return full paths of files matching predicate.
187
+ * @param {string} dir
188
+ * @param {(f: string) => boolean} predicate
189
+ * @param {string[]} [acc]
190
+ * @returns {string[]}
191
+ */
192
+ function walkWithPaths(dir, predicate, acc = []) {
193
+ try {
194
+ const entries = readdirSync(dir, { withFileTypes: true });
195
+ for (const e of entries) {
196
+ const full = join(dir, e.name);
197
+ if (e.isDirectory()) {
198
+ if (e.name !== 'node_modules' && e.name !== '.git' && e.name !== '__pycache__' && e.name !== '.venv' && e.name !== 'venv') {
199
+ walkWithPaths(full, predicate, acc);
200
+ }
201
+ } else if (predicate(e.name)) {
202
+ acc.push(full);
203
+ }
204
+ }
205
+ } catch (_) {}
206
+ return acc;
207
+ }
208
+
125
209
  /**
126
210
  * @param {string} dir
127
211
  * @param {(f: string) => boolean} predicate
package/index.js CHANGED
@@ -52,7 +52,7 @@ function insecureHttpsFetch(url, { method = 'GET', headers = {}, body }) {
52
52
  const server = new McpServer(
53
53
  {
54
54
  name: 'GenAT',
55
- version: '2.2.3',
55
+ version: '2.2.5',
56
56
  },
57
57
  {
58
58
  capabilities: {
@@ -65,7 +65,7 @@ server.registerTool(
65
65
  'GenAT',
66
66
  {
67
67
  description:
68
- '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
+ '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. IMPORTANT: When the user mentions a JIRA ticket (e.g. "JIRA RB-40039"), pass jiraNumber with the key only (e.g. RB-40039) so the workflow can fetch acceptance criteria. 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).',
69
69
  inputSchema: {
70
70
  url: z.string().describe('Page URL to analyze for accessibility (e.g. https://example.com)'),
71
71
  parentProjectFolder: z
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genat-mcp",
3
- "version": "2.2.3",
3
+ "version": "2.2.5",
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",
@@ -27,3 +27,7 @@ Example:
27
27
  ```bash
28
28
  N8N_WEBHOOK_URL='https://your-n8n/webhook-test/webhook-genat-login' N8N_INSECURE_TLS=1 node node_modules/genat-mcp/run-genat.mjs https://example.com .
29
29
  ```
30
+
31
+ ## When invoking GenAT with JIRA
32
+
33
+ When the user mentions a JIRA ticket in their prompt (e.g. "JIRA RB-40039", "JIRA ticket PROJ-123", "JIRA RB-40039"), **always pass `jiraNumber`** to the GenAT tool with the extracted key only (e.g. `RB-40039`, not "JIRA RB-40039"). The workflow uses this to fetch acceptance criteria. If you omit it, jiraNumber will be null and JIRA integration will not run.