@letsrunit/mcp-server 0.9.1 → 0.10.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/README.md +1 -0
- package/dist/index.js +79361 -70
- package/dist/index.js.map +1 -1
- package/dist/re2-EDMAKNVO.node +0 -0
- package/package.json +8 -7
- package/src/index.ts +4 -0
- package/src/tools/diagnostics.ts +22 -0
- package/src/tools/index.ts +1 -0
- package/src/tools/session-start.ts +3 -0
- package/src/utility/support.ts +238 -0
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@letsrunit/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "MCP server for letsrunit — AI-agent browser test generation and execution",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"testing",
|
|
@@ -46,12 +46,13 @@
|
|
|
46
46
|
},
|
|
47
47
|
"packageManager": "yarn@4.10.3",
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@
|
|
50
|
-
"@letsrunit/
|
|
51
|
-
"@letsrunit/
|
|
52
|
-
"@letsrunit/
|
|
53
|
-
"@letsrunit/
|
|
54
|
-
"@letsrunit/
|
|
49
|
+
"@cucumber/cucumber": "^12.7.0",
|
|
50
|
+
"@letsrunit/controller": "0.10.0",
|
|
51
|
+
"@letsrunit/gherkin": "0.10.0",
|
|
52
|
+
"@letsrunit/journal": "0.10.0",
|
|
53
|
+
"@letsrunit/playwright": "0.10.0",
|
|
54
|
+
"@letsrunit/store": "0.10.0",
|
|
55
|
+
"@letsrunit/utils": "0.10.0",
|
|
55
56
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
56
57
|
"@playwright/test": "^1.57.0",
|
|
57
58
|
"zod": "^4.3.5"
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { SessionManager } from './sessions';
|
|
|
6
6
|
const { version } = createRequire(import.meta.url)('../package.json') as { version: string };
|
|
7
7
|
import {
|
|
8
8
|
registerDebug,
|
|
9
|
+
registerDiagnostics,
|
|
9
10
|
registerDiff,
|
|
10
11
|
registerListSteps,
|
|
11
12
|
registerListSessions,
|
|
@@ -33,6 +34,9 @@ registerSessionClose(server, sessions);
|
|
|
33
34
|
registerListSteps(server, sessions);
|
|
34
35
|
registerListSessions(server, sessions);
|
|
35
36
|
registerDiff(server, sessions);
|
|
37
|
+
if (process.env.LETSRUNIT_MCP_DIAGNOSTICS === 'enabled') {
|
|
38
|
+
registerDiagnostics(server);
|
|
39
|
+
}
|
|
36
40
|
|
|
37
41
|
const transport = new StdioServerTransport();
|
|
38
42
|
await server.connect(transport);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { collectSupportDiagnostics } from '../utility/support';
|
|
3
|
+
import { err, text } from '../utility/response';
|
|
4
|
+
|
|
5
|
+
export function registerDiagnostics(server: McpServer): void {
|
|
6
|
+
server.registerTool(
|
|
7
|
+
'letsrunit_diagnostics',
|
|
8
|
+
{
|
|
9
|
+
description:
|
|
10
|
+
'Return runtime diagnostics for MCP support-file loading (cwd resolution, cucumber config path, support entries). Available only when LETSRUNIT_MCP_DIAGNOSTICS=enabled.',
|
|
11
|
+
inputSchema: {},
|
|
12
|
+
},
|
|
13
|
+
async () => {
|
|
14
|
+
try {
|
|
15
|
+
const diagnostics = await collectSupportDiagnostics();
|
|
16
|
+
return text(JSON.stringify(diagnostics));
|
|
17
|
+
} catch (e) {
|
|
18
|
+
return err(`Diagnostics failed: ${(e as Error).message}`);
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
);
|
|
22
|
+
}
|
package/src/tools/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import type { SessionManager } from '../sessions';
|
|
4
4
|
import { err, text } from '../utility/response';
|
|
5
|
+
import { loadSupportFiles } from '../utility/support';
|
|
5
6
|
|
|
6
7
|
export function registerSessionStart(server: McpServer, sessions: SessionManager): void {
|
|
7
8
|
server.registerTool(
|
|
@@ -19,6 +20,8 @@ export function registerSessionStart(server: McpServer, sessions: SessionManager
|
|
|
19
20
|
},
|
|
20
21
|
async (input) => {
|
|
21
22
|
try {
|
|
23
|
+
await loadSupportFiles();
|
|
24
|
+
|
|
22
25
|
const viewport =
|
|
23
26
|
input.viewportWidth || input.viewportHeight
|
|
24
27
|
? { width: input.viewportWidth ?? 1280, height: input.viewportHeight ?? 720 }
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { loadConfiguration } from '@cucumber/cucumber/api';
|
|
3
|
+
import { registry } from '@letsrunit/bdd';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { glob } from 'node:fs/promises';
|
|
6
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
7
|
+
import { pathToFileURL } from 'node:url';
|
|
8
|
+
|
|
9
|
+
type CucumberConfig = {
|
|
10
|
+
require?: unknown;
|
|
11
|
+
import?: unknown;
|
|
12
|
+
letsrunit?: {
|
|
13
|
+
ignore?: unknown;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type SupportEntry =
|
|
18
|
+
| { kind: 'path'; value: string }
|
|
19
|
+
| { kind: 'module'; value: string };
|
|
20
|
+
|
|
21
|
+
const CUCUMBER_CONFIG_FILES = [
|
|
22
|
+
'cucumber.js',
|
|
23
|
+
'cucumber.mjs',
|
|
24
|
+
'cucumber.cjs',
|
|
25
|
+
'cucumber.ts',
|
|
26
|
+
'cucumber.mts',
|
|
27
|
+
'cucumber.cts',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const loadedProjectRoots = new Set<string>();
|
|
31
|
+
const loadedSupportEntries = new Set<string>();
|
|
32
|
+
|
|
33
|
+
export type SupportDiagnostics = {
|
|
34
|
+
envProjectCwd: string | null;
|
|
35
|
+
processCwd: string;
|
|
36
|
+
inputCwd: string | null;
|
|
37
|
+
effectiveCwd: string;
|
|
38
|
+
projectRoot: string;
|
|
39
|
+
cucumberConfigPath: string | null;
|
|
40
|
+
supportPatterns: string[];
|
|
41
|
+
ignorePatterns: string[];
|
|
42
|
+
ignoredPaths: string[];
|
|
43
|
+
supportEntries: SupportEntry[];
|
|
44
|
+
loadedProjectRoots: string[];
|
|
45
|
+
loadedSupportEntries: string[];
|
|
46
|
+
moduleResolution: {
|
|
47
|
+
serverBddPath: string | null;
|
|
48
|
+
projectBddPath: string | null;
|
|
49
|
+
sameModule: boolean;
|
|
50
|
+
};
|
|
51
|
+
registry: {
|
|
52
|
+
total: number;
|
|
53
|
+
byType: {
|
|
54
|
+
Given: number;
|
|
55
|
+
When: number;
|
|
56
|
+
Then: number;
|
|
57
|
+
};
|
|
58
|
+
definitions: Array<{
|
|
59
|
+
type: 'Given' | 'When' | 'Then';
|
|
60
|
+
source: string;
|
|
61
|
+
comment?: string;
|
|
62
|
+
}>;
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function toStrings(value: unknown): string[] {
|
|
67
|
+
if (!Array.isArray(value)) return [];
|
|
68
|
+
return value.filter((entry): entry is string => typeof entry === 'string');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function hasGlobMagic(input: string): boolean {
|
|
72
|
+
return /[*?[\]{}]/.test(input);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isPathLike(input: string): boolean {
|
|
76
|
+
return input.startsWith('.') || input.startsWith('/') || /^[A-Za-z]:[\\/]/.test(input);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function toAbsolutePath(baseDir: string, input: string): string {
|
|
80
|
+
return isAbsolute(input) ? resolve(input) : resolve(baseDir, input);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeMatch(baseDir: string, match: string): string {
|
|
84
|
+
return isAbsolute(match) ? resolve(match) : resolve(baseDir, match);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function expandPathPatterns(baseDir: string, patterns: string[]): Promise<Set<string>> {
|
|
88
|
+
const files = new Set<string>();
|
|
89
|
+
|
|
90
|
+
for (const pattern of patterns) {
|
|
91
|
+
if (hasGlobMagic(pattern)) {
|
|
92
|
+
for await (const match of glob(pattern, { cwd: baseDir, absolute: true, withFileTypes: false })) {
|
|
93
|
+
files.add(normalizeMatch(baseDir, match));
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
files.add(toAbsolutePath(baseDir, pattern));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return files;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function resolveSupportEntries(baseDir: string, entries: string[]): Promise<SupportEntry[]> {
|
|
105
|
+
const resolved: SupportEntry[] = [];
|
|
106
|
+
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
if (hasGlobMagic(entry)) {
|
|
109
|
+
for await (const match of glob(entry, { cwd: baseDir, absolute: true, withFileTypes: false })) {
|
|
110
|
+
resolved.push({ kind: 'path', value: normalizeMatch(baseDir, match) });
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!isPathLike(entry)) {
|
|
116
|
+
resolved.push({ kind: 'module', value: entry });
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
resolved.push({ kind: 'path', value: toAbsolutePath(baseDir, entry) });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return resolved;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function findCucumberConfig(cwd: string): string | null {
|
|
127
|
+
for (const filename of CUCUMBER_CONFIG_FILES) {
|
|
128
|
+
const path = resolve(cwd, filename);
|
|
129
|
+
if (existsSync(path)) return path;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function loadLetsrunitIgnorePatterns(cwd: string): Promise<string[]> {
|
|
136
|
+
const configPath = findCucumberConfig(cwd);
|
|
137
|
+
if (!configPath) return [];
|
|
138
|
+
|
|
139
|
+
const configModule = await import(pathToFileURL(configPath).href);
|
|
140
|
+
const config = (configModule.default ?? configModule) as CucumberConfig;
|
|
141
|
+
|
|
142
|
+
return toStrings(config.letsrunit?.ignore);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function resolveEffectiveCwd(cwd?: string): string {
|
|
146
|
+
return cwd ?? process.env.LETSRUNIT_PROJECT_CWD ?? process.cwd();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function resolveFrom(moduleId: string, fromPath: string): string | null {
|
|
150
|
+
try {
|
|
151
|
+
const req = createRequire(fromPath);
|
|
152
|
+
return req.resolve(moduleId);
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function collectSupportDiagnostics(cwd?: string): Promise<SupportDiagnostics> {
|
|
159
|
+
const effectiveCwd = resolveEffectiveCwd(cwd);
|
|
160
|
+
const projectRoot = resolve(effectiveCwd);
|
|
161
|
+
const cucumberConfigPath = findCucumberConfig(projectRoot);
|
|
162
|
+
const { useConfiguration } = await loadConfiguration({}, { cwd: projectRoot });
|
|
163
|
+
const supportPatterns = [...toStrings(useConfiguration.require), ...toStrings(useConfiguration.import)];
|
|
164
|
+
const ignorePatterns = await loadLetsrunitIgnorePatterns(projectRoot);
|
|
165
|
+
const ignoredPaths = await expandPathPatterns(projectRoot, ignorePatterns);
|
|
166
|
+
const supportEntries = await resolveSupportEntries(projectRoot, supportPatterns);
|
|
167
|
+
const serverBddPath = resolveFrom('@letsrunit/bdd', import.meta.url);
|
|
168
|
+
const projectBddPath = resolveFrom('@letsrunit/bdd', resolve(projectRoot, 'package.json'));
|
|
169
|
+
const registryDefinitions = registry.defs.map((def) => ({
|
|
170
|
+
type: def.type,
|
|
171
|
+
source: def.source,
|
|
172
|
+
comment: def.comment,
|
|
173
|
+
}));
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
envProjectCwd: process.env.LETSRUNIT_PROJECT_CWD ?? null,
|
|
177
|
+
processCwd: process.cwd(),
|
|
178
|
+
inputCwd: cwd ?? null,
|
|
179
|
+
effectiveCwd,
|
|
180
|
+
projectRoot,
|
|
181
|
+
cucumberConfigPath,
|
|
182
|
+
supportPatterns,
|
|
183
|
+
ignorePatterns,
|
|
184
|
+
ignoredPaths: [...ignoredPaths].sort(),
|
|
185
|
+
supportEntries,
|
|
186
|
+
loadedProjectRoots: [...loadedProjectRoots].sort(),
|
|
187
|
+
loadedSupportEntries: [...loadedSupportEntries].sort(),
|
|
188
|
+
moduleResolution: {
|
|
189
|
+
serverBddPath,
|
|
190
|
+
projectBddPath,
|
|
191
|
+
sameModule: !!serverBddPath && !!projectBddPath && serverBddPath === projectBddPath,
|
|
192
|
+
},
|
|
193
|
+
registry: {
|
|
194
|
+
total: registryDefinitions.length,
|
|
195
|
+
byType: {
|
|
196
|
+
Given: registryDefinitions.filter((d) => d.type === 'Given').length,
|
|
197
|
+
When: registryDefinitions.filter((d) => d.type === 'When').length,
|
|
198
|
+
Then: registryDefinitions.filter((d) => d.type === 'Then').length,
|
|
199
|
+
},
|
|
200
|
+
definitions: registryDefinitions,
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function loadSupportFiles(cwd?: string): Promise<void> {
|
|
206
|
+
const projectRoot = resolve(resolveEffectiveCwd(cwd));
|
|
207
|
+
if (loadedProjectRoots.has(projectRoot)) return;
|
|
208
|
+
|
|
209
|
+
const { useConfiguration } = await loadConfiguration({}, { cwd: projectRoot });
|
|
210
|
+
const supportPatterns = [...toStrings(useConfiguration.require), ...toStrings(useConfiguration.import)];
|
|
211
|
+
if (supportPatterns.length === 0) {
|
|
212
|
+
loadedProjectRoots.add(projectRoot);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const ignorePatterns = await loadLetsrunitIgnorePatterns(projectRoot);
|
|
217
|
+
const ignoredPaths = await expandPathPatterns(projectRoot, ignorePatterns);
|
|
218
|
+
const supportEntries = await resolveSupportEntries(projectRoot, supportPatterns);
|
|
219
|
+
|
|
220
|
+
for (const entry of supportEntries) {
|
|
221
|
+
if (entry.kind === 'path' && ignoredPaths.has(entry.value)) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const key = `${entry.kind}:${entry.value}`;
|
|
226
|
+
if (loadedSupportEntries.has(key)) continue;
|
|
227
|
+
|
|
228
|
+
if (entry.kind === 'path') {
|
|
229
|
+
await import(pathToFileURL(entry.value).href);
|
|
230
|
+
} else {
|
|
231
|
+
await import(entry.value);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
loadedSupportEntries.add(key);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
loadedProjectRoots.add(projectRoot);
|
|
238
|
+
}
|