@matware/e2e-runner 1.0.2 → 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,326 +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
- if (flaky.length > 0) summary.flaky = flaky;
245
- if (failures.length > 0) summary.failures = failures;
246
-
247
- return textResult(JSON.stringify(summary, null, 2));
248
- }
249
-
250
- async function handleList(args) {
251
- const config = await loadConfig({}, args.cwd);
252
- const suites = listSuites(config.testsDir);
253
-
254
- if (suites.length === 0) {
255
- return textResult('No test suites found in ' + config.testsDir);
256
- }
257
-
258
- const lines = suites.map(s =>
259
- `${s.name} (${s.testCount} tests): ${s.tests.join(', ')}`
260
- );
261
-
262
- return textResult(lines.join('\n'));
263
- }
264
-
265
- async function handleCreateTest(args) {
266
- const config = await loadConfig({}, args.cwd);
267
-
268
- if (!fs.existsSync(config.testsDir)) {
269
- fs.mkdirSync(config.testsDir, { recursive: true });
270
- }
271
-
272
- const filename = args.name.endsWith('.json') ? args.name : `${args.name}.json`;
273
- const filePath = path.join(config.testsDir, filename);
274
-
275
- if (fs.existsSync(filePath)) {
276
- return errorResult(`File already exists: ${filePath}`);
277
- }
278
-
279
- let content;
280
- if (args.hooks && Object.values(args.hooks).some(h => h?.length > 0)) {
281
- content = { hooks: args.hooks, tests: args.tests };
282
- } else {
283
- content = args.tests;
284
- }
285
-
286
- fs.writeFileSync(filePath, JSON.stringify(content, null, 2) + '\n');
287
- return textResult(`Created test file: ${filePath}\n\n${args.tests.length} test(s) defined.`);
288
- }
289
-
290
- async function handlePoolStatus(args) {
291
- const config = await loadConfig({}, args.cwd);
292
- const status = await getPoolStatus(config.poolUrl);
293
-
294
- const lines = [
295
- `Available: ${status.available ? 'yes' : 'no'}`,
296
- `Running: ${status.running}/${status.maxConcurrent}`,
297
- `Queued: ${status.queued}`,
298
- `Sessions: ${status.sessions.length}`,
299
- ];
300
-
301
- if (status.error) {
302
- lines.push(`Error: ${status.error}`);
303
- }
304
-
305
- return textResult(lines.join('\n'));
306
- }
307
-
308
- async function handlePoolStart(args) {
309
- const overrides = {};
310
- if (args.port) overrides.poolPort = args.port;
311
- if (args.maxSessions) overrides.maxSessions = args.maxSessions;
312
-
313
- const config = await loadConfig(overrides, args.cwd);
314
- startPool(config, args.cwd);
315
- return textResult(`Chrome pool started on port ${config.poolPort}`);
316
- }
317
-
318
- async function handlePoolStop(args) {
319
- const config = await loadConfig({}, args.cwd);
320
- stopPool(config, args.cwd);
321
- return textResult('Chrome pool stopped');
322
- }
323
-
324
- // ── Helpers ───────────────────────────────────────────────────────────────────
325
-
326
- function textResult(text) {
327
- return { content: [{ type: 'text', text }] };
328
- }
329
-
330
- function errorResult(message) {
331
- return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
332
- }
333
-
334
29
  // ── Server setup ──────────────────────────────────────────────────────────────
335
30
 
336
31
  export async function startMcpServer() {
337
32
  const server = new Server(
338
- { name: 'e2e-runner', version: '1.0.0' },
33
+ { name: 'e2e-runner', version: VERSION },
339
34
  { capabilities: { tools: {} } }
340
35
  );
341
36
 
@@ -347,22 +42,7 @@ export async function startMcpServer() {
347
42
  const { name, arguments: args = {} } = request.params;
348
43
 
349
44
  try {
350
- switch (name) {
351
- case 'e2e_run':
352
- return await handleRun(args);
353
- case 'e2e_list':
354
- return await handleList(args);
355
- case 'e2e_create_test':
356
- return await handleCreateTest(args);
357
- case 'e2e_pool_status':
358
- return await handlePoolStatus(args);
359
- case 'e2e_pool_start':
360
- return await handlePoolStart(args);
361
- case 'e2e_pool_stop':
362
- return await handlePoolStop(args);
363
- default:
364
- return errorResult(`Unknown tool: ${name}`);
365
- }
45
+ return await dispatchTool(name, args);
366
46
  } catch (error) {
367
47
  return errorResult(error.message);
368
48
  }