@matware/e2e-runner 1.0.3 → 1.1.0

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/src/mcp-server.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * MCP Server for @matware/e2e-runner
2
+ * MCP Server for @matware/e2e-runner (stdio transport)
3
3
  *
4
4
  * Exposes E2E test runner capabilities as MCP tools so Claude Code
5
5
  * (and any MCP-compatible client) can run tests, list suites,
@@ -16,335 +16,21 @@ import {
16
16
  CallToolRequestSchema,
17
17
  } from '@modelcontextprotocol/sdk/types.js';
18
18
 
19
- import fs from 'fs';
20
- import path from 'path';
19
+ import { createRequire } from 'module';
21
20
 
22
- import { loadConfig } from './config.js';
23
- import { waitForPool, getPoolStatus, startPool, stopPool } from './pool.js';
24
- import { runTestsParallel, loadTestFile, loadTestSuite, loadAllSuites, listSuites } from './runner.js';
25
- import { generateReport, saveReport } from './reporter.js';
21
+ const require = createRequire(import.meta.url);
22
+ const { version: VERSION } = require('../package.json');
23
+
24
+ import { TOOLS, dispatchTool, errorResult } from './mcp-tools.js';
26
25
 
27
26
  // ── Redirect console.log to stderr so it doesn't corrupt the MCP stdio protocol ──
28
27
  console.log = (...args) => process.stderr.write(args.join(' ') + '\n');
29
28
 
30
- // ── Tool definitions ──────────────────────────────────────────────────────────
31
-
32
- const TOOLS = [
33
- {
34
- name: 'e2e_run',
35
- description:
36
- 'Run E2E browser tests. Specify "all" to run every suite, "suite" for a specific suite, or "file" for a JSON file path. Returns structured results with pass/fail status, duration, and error details.',
37
- inputSchema: {
38
- type: 'object',
39
- properties: {
40
- all: {
41
- type: 'boolean',
42
- description: 'Run all test suites from the tests directory',
43
- },
44
- suite: {
45
- type: 'string',
46
- description: 'Suite name to run (e.g. "auth", "01-login"). Matches with or without numeric prefix.',
47
- },
48
- file: {
49
- type: 'string',
50
- description: 'Absolute or relative path to a JSON test file',
51
- },
52
- concurrency: {
53
- type: 'number',
54
- description: 'Number of parallel workers (default from config)',
55
- },
56
- baseUrl: {
57
- type: 'string',
58
- description: 'Override the base URL for this run',
59
- },
60
- retries: {
61
- type: 'number',
62
- description: 'Number of retries for failed tests',
63
- },
64
- cwd: {
65
- type: 'string',
66
- description: 'Absolute path to the project root directory. Claude Code should pass its current working directory.',
67
- },
68
- },
69
- },
70
- },
71
- {
72
- name: 'e2e_list',
73
- description:
74
- 'List all available E2E test suites with their test names and counts.',
75
- inputSchema: {
76
- type: 'object',
77
- properties: {
78
- cwd: {
79
- type: 'string',
80
- description: 'Absolute path to the project root directory. Claude Code should pass its current working directory.',
81
- },
82
- },
83
- },
84
- },
85
- {
86
- name: 'e2e_create_test',
87
- description:
88
- 'Create a new E2E test JSON file. Provide the suite name and an array of test objects, each with a name and actions array.',
89
- inputSchema: {
90
- type: 'object',
91
- properties: {
92
- name: {
93
- type: 'string',
94
- description: 'Suite file name without .json extension (e.g. "login", "05-checkout")',
95
- },
96
- tests: {
97
- type: 'array',
98
- description: 'Array of test objects with { name, actions }',
99
- items: {
100
- type: 'object',
101
- properties: {
102
- name: { type: 'string', description: 'Test name' },
103
- actions: {
104
- type: 'array',
105
- description: 'Sequential browser actions',
106
- items: {
107
- type: 'object',
108
- properties: {
109
- type: {
110
- type: 'string',
111
- description: 'Action type: goto, click, type, wait, assert_text, assert_url, assert_visible, assert_count, screenshot, select, clear, press, scroll, hover, evaluate',
112
- },
113
- selector: { type: 'string', description: 'CSS selector' },
114
- value: { type: 'string', description: 'Value for the action' },
115
- text: { type: 'string', description: 'Text content to match' },
116
- },
117
- required: ['type'],
118
- },
119
- },
120
- },
121
- required: ['name', 'actions'],
122
- },
123
- },
124
- hooks: {
125
- type: 'object',
126
- description: 'Optional hooks: beforeAll, afterAll, beforeEach, afterEach (each an array of actions)',
127
- properties: {
128
- beforeAll: { type: 'array', items: { type: 'object' } },
129
- afterAll: { type: 'array', items: { type: 'object' } },
130
- beforeEach: { type: 'array', items: { type: 'object' } },
131
- afterEach: { type: 'array', items: { type: 'object' } },
132
- },
133
- },
134
- cwd: {
135
- type: 'string',
136
- description: 'Absolute path to the project root directory. Claude Code should pass its current working directory.',
137
- },
138
- },
139
- required: ['name', 'tests'],
140
- },
141
- },
142
- {
143
- name: 'e2e_pool_status',
144
- description:
145
- 'Get the status of the Chrome pool (browserless/chrome). Shows availability, running sessions, capacity, and queued requests.',
146
- inputSchema: {
147
- type: 'object',
148
- properties: {
149
- cwd: {
150
- type: 'string',
151
- description: 'Absolute path to the project root directory. Claude Code should pass its current working directory.',
152
- },
153
- },
154
- },
155
- },
156
- {
157
- name: 'e2e_pool_start',
158
- description:
159
- 'Start the Chrome pool (browserless/chrome Docker container). Requires Docker to be running.',
160
- inputSchema: {
161
- type: 'object',
162
- properties: {
163
- port: {
164
- type: 'number',
165
- description: 'Port for the pool (default 3333)',
166
- },
167
- maxSessions: {
168
- type: 'number',
169
- description: 'Max concurrent Chrome sessions (default 10)',
170
- },
171
- cwd: {
172
- type: 'string',
173
- description: 'Absolute path to the project root directory. Claude Code should pass its current working directory.',
174
- },
175
- },
176
- },
177
- },
178
- {
179
- name: 'e2e_pool_stop',
180
- description: 'Stop the Chrome pool Docker container.',
181
- inputSchema: {
182
- type: 'object',
183
- properties: {
184
- cwd: {
185
- type: 'string',
186
- description: 'Absolute path to the project root directory. Claude Code should pass its current working directory.',
187
- },
188
- },
189
- },
190
- },
191
- ];
192
-
193
- // ── Tool handlers ─────────────────────────────────────────────────────────────
194
-
195
- async function handleRun(args) {
196
- const configOverrides = {};
197
- if (args.concurrency) configOverrides.concurrency = args.concurrency;
198
- if (args.baseUrl) configOverrides.baseUrl = args.baseUrl;
199
- if (args.retries !== undefined) configOverrides.retries = args.retries;
200
-
201
- const config = await loadConfig(configOverrides, args.cwd);
202
-
203
- await waitForPool(config.poolUrl);
204
-
205
- let tests, hooks;
206
-
207
- if (args.all) {
208
- ({ tests, hooks } = loadAllSuites(config.testsDir));
209
- } else if (args.suite) {
210
- ({ tests, hooks } = loadTestSuite(args.suite, config.testsDir));
211
- } else if (args.file) {
212
- const cwd = args.cwd || process.cwd();
213
- const filePath = path.isAbsolute(args.file) ? args.file : path.resolve(cwd, args.file);
214
- ({ tests, hooks } = loadTestFile(filePath));
215
- } else {
216
- return errorResult('Provide one of: all (true), suite (name), or file (path)');
217
- }
218
-
219
- if (tests.length === 0) {
220
- return errorResult('No tests found');
221
- }
222
-
223
- const results = await runTestsParallel(tests, config, hooks || {});
224
- const report = generateReport(results);
225
- saveReport(report, config.screenshotsDir, config);
226
-
227
- const failures = report.results
228
- .filter(r => !r.success)
229
- .map(r => ({
230
- name: r.name,
231
- error: r.error,
232
- errorScreenshot: r.errorScreenshot || null,
233
- }));
234
-
235
- const flaky = report.results
236
- .filter(r => r.success && r.attempt > 1)
237
- .map(r => ({ name: r.name, attempts: r.attempt }));
238
-
239
- const summary = {
240
- ...report.summary,
241
- reportPath: path.join(config.screenshotsDir, 'report.json'),
242
- };
243
-
244
- const consoleErrors = report.results
245
- .filter(r => r.consoleLogs?.some(l => l.type === 'error' || l.type === 'warning'))
246
- .map(r => ({ name: r.name, logs: r.consoleLogs.filter(l => l.type === 'error' || l.type === 'warning') }));
247
- const networkErrors = report.results
248
- .filter(r => r.networkErrors?.length > 0)
249
- .map(r => ({ name: r.name, errors: r.networkErrors }));
250
-
251
- if (flaky.length > 0) summary.flaky = flaky;
252
- if (failures.length > 0) summary.failures = failures;
253
- if (consoleErrors.length > 0) summary.consoleErrors = consoleErrors;
254
- if (networkErrors.length > 0) summary.networkErrors = networkErrors;
255
-
256
- return textResult(JSON.stringify(summary, null, 2));
257
- }
258
-
259
- async function handleList(args) {
260
- const config = await loadConfig({}, args.cwd);
261
- const suites = listSuites(config.testsDir);
262
-
263
- if (suites.length === 0) {
264
- return textResult('No test suites found in ' + config.testsDir);
265
- }
266
-
267
- const lines = suites.map(s =>
268
- `${s.name} (${s.testCount} tests): ${s.tests.join(', ')}`
269
- );
270
-
271
- return textResult(lines.join('\n'));
272
- }
273
-
274
- async function handleCreateTest(args) {
275
- const config = await loadConfig({}, args.cwd);
276
-
277
- if (!fs.existsSync(config.testsDir)) {
278
- fs.mkdirSync(config.testsDir, { recursive: true });
279
- }
280
-
281
- const filename = args.name.endsWith('.json') ? args.name : `${args.name}.json`;
282
- const filePath = path.join(config.testsDir, filename);
283
-
284
- if (fs.existsSync(filePath)) {
285
- return errorResult(`File already exists: ${filePath}`);
286
- }
287
-
288
- let content;
289
- if (args.hooks && Object.values(args.hooks).some(h => h?.length > 0)) {
290
- content = { hooks: args.hooks, tests: args.tests };
291
- } else {
292
- content = args.tests;
293
- }
294
-
295
- fs.writeFileSync(filePath, JSON.stringify(content, null, 2) + '\n');
296
- return textResult(`Created test file: ${filePath}\n\n${args.tests.length} test(s) defined.`);
297
- }
298
-
299
- async function handlePoolStatus(args) {
300
- const config = await loadConfig({}, args.cwd);
301
- const status = await getPoolStatus(config.poolUrl);
302
-
303
- const lines = [
304
- `Available: ${status.available ? 'yes' : 'no'}`,
305
- `Running: ${status.running}/${status.maxConcurrent}`,
306
- `Queued: ${status.queued}`,
307
- `Sessions: ${status.sessions.length}`,
308
- ];
309
-
310
- if (status.error) {
311
- lines.push(`Error: ${status.error}`);
312
- }
313
-
314
- return textResult(lines.join('\n'));
315
- }
316
-
317
- async function handlePoolStart(args) {
318
- const overrides = {};
319
- if (args.port) overrides.poolPort = args.port;
320
- if (args.maxSessions) overrides.maxSessions = args.maxSessions;
321
-
322
- const config = await loadConfig(overrides, args.cwd);
323
- startPool(config, args.cwd);
324
- return textResult(`Chrome pool started on port ${config.poolPort}`);
325
- }
326
-
327
- async function handlePoolStop(args) {
328
- const config = await loadConfig({}, args.cwd);
329
- stopPool(config, args.cwd);
330
- return textResult('Chrome pool stopped');
331
- }
332
-
333
- // ── Helpers ───────────────────────────────────────────────────────────────────
334
-
335
- function textResult(text) {
336
- return { content: [{ type: 'text', text }] };
337
- }
338
-
339
- function errorResult(message) {
340
- return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
341
- }
342
-
343
29
  // ── Server setup ──────────────────────────────────────────────────────────────
344
30
 
345
31
  export async function startMcpServer() {
346
32
  const server = new Server(
347
- { name: 'e2e-runner', version: '1.0.0' },
33
+ { name: 'e2e-runner', version: VERSION },
348
34
  { capabilities: { tools: {} } }
349
35
  );
350
36
 
@@ -356,22 +42,7 @@ export async function startMcpServer() {
356
42
  const { name, arguments: args = {} } = request.params;
357
43
 
358
44
  try {
359
- switch (name) {
360
- case 'e2e_run':
361
- return await handleRun(args);
362
- case 'e2e_list':
363
- return await handleList(args);
364
- case 'e2e_create_test':
365
- return await handleCreateTest(args);
366
- case 'e2e_pool_status':
367
- return await handlePoolStatus(args);
368
- case 'e2e_pool_start':
369
- return await handlePoolStart(args);
370
- case 'e2e_pool_stop':
371
- return await handlePoolStop(args);
372
- default:
373
- return errorResult(`Unknown tool: ${name}`);
374
- }
45
+ return await dispatchTool(name, args);
375
46
  } catch (error) {
376
47
  return errorResult(error.message);
377
48
  }