genat-mcp 2.2.8 → 2.3.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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  MCP server that exposes a **GenAT** tool: generate accessibility tests for a URL based on your project folder. It detects framework (TypeScript, JavaScript, or Python, BDD, Page Object), calls an n8n GenAT workflow, and returns (or writes) generated test files.
4
4
 
5
- Full setup (n8n workflow, services, usage) is in the [project README](../README.md).
5
+ Full setup (n8n workflow, services, usage) is in the [project README](../README.md). GenAT workflows also instruct the AI to align with **OCLC internal Confluence** pages ([Accessibility Guidelines and Resources](https://confluence.oclc.org/spaces/UAD/pages/103062358/Accessibility+Guidelines+and+Resources), [OCLC Accessibility Release Blockers](https://confluence.oclc.org/spaces/UXUI/pages/119232333/OCLC+Accessibility+Release+Blockers)); the MCP sends optional extra context from [oclc-accessibility-confluence-context.md](oclc-accessibility-confluence-context.md) or `GENAT_OCLC_ACCESSIBILITY_CONTEXT`.
6
6
 
7
7
  **For teams:** See [GENAT_OVERVIEW.md](GENAT_OVERVIEW.md) for a concise, presentation-ready overview. Suitable as a source for NotebookLM or other AI tools to create slide decks.
8
8
 
@@ -42,6 +42,7 @@ npm install /path/to/n8n_playwright_tests/mcp-genat
42
42
  | **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. |
43
43
  | **GENAT_PROJECT_ROOT** | No | (cwd) | Base path for resolving `parentProjectFolder`. When set, `parentProjectFolder` is resolved relative to this path. Use when your project is outside the MCP's working directory. Example: if your pytest project is at `/Users/me/projects/ill_tests`, set `GENAT_PROJECT_ROOT=/Users/me/projects` and use `parentProjectFolder ill_tests` in the prompt. |
44
44
  | **GENAT_FETCH_TIMEOUT_MS** | No | 600000 (10 min) | Max time in milliseconds to wait for the n8n webhook response. Increase if GenAT times out before the workflow completes (e.g. complex pages with JIRA + AI Agent). Set in MCP env, e.g. `GENAT_FETCH_TIMEOUT_MS=900000` for 15 minutes. |
45
+ | **GENAT_OCLC_ACCESSIBILITY_CONTEXT** | No | (bundled file) | Optional multiline text sent as `oclcAccessibilityContext` in the webhook body. Overrides the default [oclc-accessibility-confluence-context.md](oclc-accessibility-confluence-context.md) (OCLC UAD guidelines + UXUI release blockers links and notes). Use to paste updated Confluence excerpts without republishing the package. |
45
46
 
46
47
  ## Framework detection and auto-discovery
47
48
 
package/index.js CHANGED
@@ -12,6 +12,7 @@ import { z } from 'zod';
12
12
  import { detectFramework } from './detect-framework.js';
13
13
  import { detectLogin } from './detect-login.js';
14
14
  import { writeGeneratedFiles } from './write-files.js';
15
+ import { loadOclcAccessibilityContext } from './oclc-context.js';
15
16
 
16
17
  const N8N_WEBHOOK_URL = process.env.N8N_WEBHOOK_URL || 'http://localhost:5678/webhook-test/webhook-genat-login';
17
18
  const SERVICE_BASE_URL = process.env.SERVICE_BASE_URL || null;
@@ -55,7 +56,7 @@ function insecureHttpsFetch(url, { method = 'GET', headers = {}, body, signal })
55
56
  const server = new McpServer(
56
57
  {
57
58
  name: 'GenAT',
58
- version: '2.2.8',
59
+ version: '2.3.1',
59
60
  },
60
61
  {
61
62
  capabilities: {
@@ -102,13 +103,38 @@ server.registerTool(
102
103
  .string()
103
104
  .optional()
104
105
  .describe('Login page URL if different from target URL'),
106
+ requiresAuth: z
107
+ .boolean()
108
+ .optional()
109
+ .describe(
110
+ '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.'
111
+ ),
112
+ forceLogin: z
113
+ .boolean()
114
+ .optional()
115
+ .describe(
116
+ 'v2 workflow only: same behavior as requiresAuth when true (optional alternate flag for tooling).'
117
+ ),
105
118
  jiraNumber: z
106
119
  .string()
107
120
  .optional()
108
121
  .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".'),
109
122
  },
110
123
  },
111
- async ({ url, parentProjectFolder, writeToProject, maxAssertions, scopeToRegions, analyzeStates, loginUsername, loginPassword, loginUrl, jiraNumber }) => {
124
+ async ({
125
+ url,
126
+ parentProjectFolder,
127
+ writeToProject,
128
+ maxAssertions,
129
+ scopeToRegions,
130
+ analyzeStates,
131
+ loginUsername,
132
+ loginPassword,
133
+ loginUrl,
134
+ requiresAuth,
135
+ forceLogin,
136
+ jiraNumber,
137
+ }) => {
112
138
  if (!url || typeof url !== 'string') {
113
139
  return {
114
140
  content: [{ type: 'text', text: JSON.stringify({ error: 'Missing or invalid "url"' }) }],
@@ -151,6 +177,7 @@ server.registerTool(
151
177
  const maxAssertionsResolved = maxAssertions ?? (process.env.N8N_MAX_ASSERTIONS ? parseInt(process.env.N8N_MAX_ASSERTIONS, 10) : null);
152
178
  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);
153
179
  const analyzeStatesResolved = analyzeStates || /^1|true|yes$/i.test(process.env.N8N_ANALYZE_STATES || '');
180
+ const oclcAccessibilityContext = loadOclcAccessibilityContext();
154
181
 
155
182
  const body = {
156
183
  url,
@@ -159,6 +186,7 @@ server.registerTool(
159
186
  pageObject: framework.pageObject,
160
187
  projectSummary: framework.projectSummary,
161
188
  ...(framework.projectLayoutHints && { projectLayoutHints: framework.projectLayoutHints }),
189
+ ...(oclcAccessibilityContext && { oclcAccessibilityContext }),
162
190
  ...(SERVICE_BASE_URL && { serviceBaseUrl: SERVICE_BASE_URL }),
163
191
  ...(maxAssertionsResolved != null && !isNaN(maxAssertionsResolved) && { maxAssertions: maxAssertionsResolved }),
164
192
  ...(scopeToRegionsResolved?.length && { scopeToRegions: scopeToRegionsResolved }),
@@ -171,6 +199,8 @@ server.registerTool(
171
199
  ...(login.passwordSelector && { loginPasswordSelector: login.passwordSelector }),
172
200
  ...(login.submitSelector && { loginSubmitSelector: login.submitSelector }),
173
201
  }),
202
+ ...(requiresAuth === true && { requiresAuth: true }),
203
+ ...(forceLogin === true && { forceLogin: true }),
174
204
  ...(jiraNumber && jiraNumber.trim() && { jiraNumber: jiraNumber.trim() }),
175
205
  };
176
206
 
@@ -0,0 +1,51 @@
1
+ # OCLC internal accessibility sources (GenAT)
2
+
3
+ GenAT injects this file into the n8n workflow as **oclcAccessibilityContext** so generated tests align with internal guidance, in addition to WCAG 2.1 AA and related standards from the accessibility analyzer.
4
+
5
+ Source URLs (canonical):
6
+
7
+ - **UAD — Accessibility Guidelines and Resources:** https://confluence.oclc.org/spaces/UAD/pages/103062358/Accessibility+Guidelines+and+Resources
8
+ - **UXUI — OCLC Accessibility Release Blockers:** https://confluence.oclc.org/spaces/UXUI/pages/119232333/OCLC+Accessibility+Release+Blockers
9
+
10
+ ---
11
+
12
+ ## Summary: UAD Accessibility Guidelines and Resources
13
+
14
+ **Policy / Definition of done:** Net-new applications (new UI stack, e.g. React/Angular with Material UI) are expected to be accessible and internationalized. Interfaces must be usable and inclusive.
15
+
16
+ **Principles:** Follow **POUR** — Perceivable, Operable, Understandable, Robust. Accessibility is broader than screen-reader-only compliance.
17
+
18
+ **Build (high level):**
19
+
20
+ - Use Storybook with accessibility tooling where applicable.
21
+ - Valid, well-structured HTML; landmark regions; semantic elements (`main`, `nav`, headings, etc.) over generic `div`/`span` when defining structure.
22
+ - Relative sizing (**rem**/**em**), sensible line-height; avoid locking users out of zoom.
23
+ - Prefer **native** HTML controls (`button`, `a`, `input`, `select`, `form`) over custom reimplementations.
24
+ - Use `<a>` for navigation (with real `href`); use `<button>` for actions (style as link if needed).
25
+ - All images need appropriate **alt** text.
26
+ - Use **ARIA** correctly; incorrect ARIA can harm more than help.
27
+ - **Keyboard:** no traps; focus management after dynamic changes; follow WAI-ARIA authoring practices for composite widgets.
28
+ - Tab order: interactive controls in a logical sequence; avoid `tabindex` on non-interactive elements except focus management (`tabindex="-1"` when appropriate).
29
+ - Treat a11y issues in code review similarly to i18n/hardcoded strings.
30
+
31
+ **Verify:** Target **WCAG 2.1 AA** plus real-world usability. Combine automated checks (e.g. axe, Pa11y), linting (e.g. eslint-plugin-jsx-a11y), integration/unit tests for behaviors (e.g. modal focus trap), browser tools, and **manual** testing including screen readers (VoiceOver, NVDA) with realistic navigation patterns.
32
+
33
+ ---
34
+
35
+ ## Summary: OCLC Accessibility Release Blockers
36
+
37
+ **Intent:** Distinguish **critical failures** that must block a release vs. issues that can be fixed post-release.
38
+
39
+ **The three release blockers** (minimum for OCLC interfaces):
40
+
41
+ 1. **Keyboard operable** — Everything operable without a mouse; no keyboard traps. Primary WCAG tie-in: **2.1.1 Keyboard**. Related: meaningful sequence, no keyboard trap, bypass blocks, focus order, focus visible.
42
+ 2. **High contrast** — Text **4.5:1**; non-text UI (icons, borders) **3:1** vs. background. Check hover/focus/selected states. Primary: **1.4.3 Contrast (Minimum)**. Related: use of color, non-text contrast.
43
+ 3. **Clearly labeled** — Text alternatives for non-text content; forms and controls have clear, distinguishable names/labels. Primary: **1.1.1 Non-text Content**. Related: info/relationships, link purpose, headings/labels, labels/instructions, name/role/value.
44
+
45
+ OCLC aims for **WCAG 2.2 A and AA** broadly; prioritize **site-wide** issues (login, headers/footers, navigation, reusable components, contrast) when triaging.
46
+
47
+ **For GenAT:** Generated tests should explicitly exercise these three themes (keyboard paths, contrast-relevant assertions where feasible, labels/roles/names) in addition to axe rule coverage.
48
+
49
+ ---
50
+
51
+ Teams may override this file via env **`GENAT_OCLC_ACCESSIBILITY_CONTEXT`** to paste updated Confluence excerpts without republishing the package.
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Optional OCLC Confluence text appended to the n8n webhook body as oclcAccessibilityContext.
3
+ * Env GENAT_OCLC_ACCESSIBILITY_CONTEXT overrides the default file.
4
+ */
5
+ import { readFileSync, existsSync } from 'fs';
6
+ import { join, dirname } from 'path';
7
+ import { fileURLToPath } from 'url';
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const DEFAULT_FILE = join(__dirname, 'oclc-accessibility-confluence-context.md');
11
+ const MAX_LEN = 120000;
12
+
13
+ export function loadOclcAccessibilityContext() {
14
+ const fromEnv = process.env.GENAT_OCLC_ACCESSIBILITY_CONTEXT?.trim();
15
+ if (fromEnv) return fromEnv.slice(0, MAX_LEN);
16
+ if (existsSync(DEFAULT_FILE)) {
17
+ try {
18
+ return readFileSync(DEFAULT_FILE, 'utf8').trim().slice(0, MAX_LEN);
19
+ } catch {
20
+ return '';
21
+ }
22
+ }
23
+ return '';
24
+ }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "genat-mcp",
3
- "version": "2.2.8",
3
+ "version": "2.3.1",
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", "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", "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",
package/run-genat.mjs CHANGED
@@ -13,12 +13,15 @@
13
13
  * N8N_MAX_ASSERTIONS - override assertion count (e.g. 25)
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
+ * 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)
16
18
  */
17
19
  import https from 'node:https';
18
20
  import { resolve } from 'node:path';
19
21
  import { detectFramework } from './detect-framework.js';
20
22
  import { detectLogin } from './detect-login.js';
21
23
  import { writeGeneratedFiles } from './write-files.js';
24
+ import { loadOclcAccessibilityContext } from './oclc-context.js';
22
25
 
23
26
  const DEFAULT_WEBHOOK = 'http://localhost:5678/webhook-test/webhook-genat-login';
24
27
  const N8N_WEBHOOK_URL = process.env.N8N_WEBHOOK_URL || DEFAULT_WEBHOOK;
@@ -85,6 +88,9 @@ async function main() {
85
88
  ? process.env.N8N_SCOPE_TO_REGIONS.split(',').map((s) => s.trim()).filter(Boolean)
86
89
  : null;
87
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 || '');
93
+ const oclcAccessibilityContext = loadOclcAccessibilityContext();
88
94
 
89
95
  const body = {
90
96
  url,
@@ -93,6 +99,7 @@ async function main() {
93
99
  pageObject: framework.pageObject,
94
100
  projectSummary: framework.projectSummary,
95
101
  ...(framework.projectLayoutHints && { projectLayoutHints: framework.projectLayoutHints }),
102
+ ...(oclcAccessibilityContext && { oclcAccessibilityContext }),
96
103
  ...(maxAssertions != null && !isNaN(maxAssertions) && { maxAssertions }),
97
104
  ...(scopeToRegions?.length && { scopeToRegions }),
98
105
  ...(analyzeStates && { analyzeStates: true }),
@@ -102,6 +109,8 @@ async function main() {
102
109
  ...(login.usernameSelector && { loginUsernameSelector: login.usernameSelector }),
103
110
  ...(login.passwordSelector && { loginPasswordSelector: login.passwordSelector }),
104
111
  ...(login.submitSelector && { loginSubmitSelector: login.submitSelector }),
112
+ ...(requiresAuth && { requiresAuth: true }),
113
+ ...(forceLogin && { forceLogin: true }),
105
114
  };
106
115
 
107
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
@@ -5,6 +5,7 @@
5
5
  import { existsSync } from 'fs';
6
6
  import { writeFile, mkdir } from 'fs/promises';
7
7
  import { dirname, join, resolve } from 'path';
8
+ import { stripLeadingGherkinFromPythonSteps } from './strip-gherkin-from-python-steps.js';
8
9
 
9
10
  const DEFAULT_DIR = 'tests/accessibility';
10
11
 
@@ -54,10 +55,14 @@ export async function writeGeneratedFiles(parentProjectFolder, data) {
54
55
 
55
56
  if (usePytestBddLayout) {
56
57
  const pb = resolvePytestBddPaths(root);
58
+ const stepDefStripped = data.stepDefCode
59
+ ? stripLeadingGherkinFromPythonSteps(data.stepDefCode)
60
+ : '';
61
+ const testStripped = data.testCode ? stripLeadingGherkinFromPythonSteps(data.testCode) : '';
57
62
  const dup =
58
- data.testCode &&
59
- data.stepDefCode &&
60
- data.testCode.trim() === data.stepDefCode.trim();
63
+ testStripped &&
64
+ stepDefStripped &&
65
+ testStripped.trim() === stepDefStripped.trim();
61
66
 
62
67
  if (data.featureFile) {
63
68
  const relPath = suggested.feature || pb.featureRel;
@@ -71,14 +76,14 @@ export async function writeGeneratedFiles(parentProjectFolder, data) {
71
76
  const relPath = suggested.stepDef || pb.stepRel;
72
77
  const abs = join(root, relPath);
73
78
  await mkdir(dirname(abs), { recursive: true });
74
- await writeFile(abs, data.stepDefCode || data.testCode || '', 'utf8');
79
+ await writeFile(abs, stepDefStripped || testStripped || '', 'utf8');
75
80
  written.push(relPath);
76
81
  } else {
77
82
  if (data.stepDefCode) {
78
83
  const relPath = suggested.stepDef || pb.stepRel;
79
84
  const abs = join(root, relPath);
80
85
  await mkdir(dirname(abs), { recursive: true });
81
- await writeFile(abs, data.stepDefCode, 'utf8');
86
+ await writeFile(abs, stepDefStripped, 'utf8');
82
87
  written.push(relPath);
83
88
  }
84
89
  if (data.testCode) {
@@ -86,13 +91,13 @@ export async function writeGeneratedFiles(parentProjectFolder, data) {
86
91
  const relPath = suggested.spec || pb.specRel;
87
92
  const abs = join(root, relPath);
88
93
  await mkdir(dirname(abs), { recursive: true });
89
- await writeFile(abs, data.testCode, 'utf8');
94
+ await writeFile(abs, testStripped, 'utf8');
90
95
  written.push(relPath);
91
96
  } else {
92
97
  const relPath = suggested.stepDef || pb.stepRel;
93
98
  const abs = join(root, relPath);
94
99
  await mkdir(dirname(abs), { recursive: true });
95
- await writeFile(abs, data.testCode, 'utf8');
100
+ await writeFile(abs, testStripped, 'utf8');
96
101
  written.push(relPath);
97
102
  }
98
103
  }