@letsrunit/mcp-server 0.14.4 → 0.15.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/index.ts CHANGED
@@ -2,10 +2,10 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
3
  import { bootstrapProjectServer } from './bootstrap';
4
4
 
5
- declare const __LETSRUNIT_VERSION__: string;
5
+ declare const __LETSRUNIT_VERSION__: string | undefined;
6
6
 
7
7
  const version = typeof __LETSRUNIT_VERSION__ === 'string' ? __LETSRUNIT_VERSION__ : 'unknown';
8
- bootstrapProjectServer();
8
+ const runtimeMode = bootstrapProjectServer();
9
9
 
10
10
  const { SessionManager } = await import('./sessions');
11
11
  const {
@@ -14,6 +14,7 @@ const {
14
14
  registerDiff,
15
15
  registerListSteps,
16
16
  registerListSessions,
17
+ registerReload,
17
18
  registerRun,
18
19
  registerScreenshot,
19
20
  registerSessionClose,
@@ -29,7 +30,7 @@ const server = new McpServer({
29
30
  websiteUrl: 'https://letsrunit.ai',
30
31
  });
31
32
 
32
- registerSessionStart(server, sessions);
33
+ registerSessionStart(server, sessions, { runtimeMode });
33
34
  registerRun(server, sessions);
34
35
  registerSnapshot(server, sessions);
35
36
  registerScreenshot(server, sessions);
@@ -37,6 +38,7 @@ registerDebug(server, sessions);
37
38
  registerSessionClose(server, sessions);
38
39
  registerListSteps(server, sessions);
39
40
  registerListSessions(server, sessions);
41
+ registerReload(server, { runtimeMode });
40
42
  registerDiff(server, sessions);
41
43
  if (process.env.LETSRUNIT_MCP_DIAGNOSTICS === 'enabled') {
42
44
  registerDiagnostics(server, sessions);
@@ -1,7 +1,7 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
3
  import type { SessionManager } from '../sessions';
4
- import { collectSupportDiagnostics } from '../utility/support';
4
+ import { collectDiagnostics } from '../utility/diagnostics';
5
5
  import { err, text } from '../utility/response';
6
6
 
7
7
  export function registerDiagnostics(server: McpServer, sessions: SessionManager): void {
@@ -16,7 +16,7 @@ export function registerDiagnostics(server: McpServer, sessions: SessionManager)
16
16
  },
17
17
  async (input) => {
18
18
  try {
19
- const diagnostics = await collectSupportDiagnostics();
19
+ const diagnostics = await collectDiagnostics();
20
20
  const session = sessions.get(input.sessionId);
21
21
  const sessionInfo = {
22
22
  sessionId: session.id,
@@ -4,6 +4,7 @@ export { registerDiff } from './diff';
4
4
  export { registerListSteps } from './list-steps';
5
5
  export { registerListSessions } from './list-sessions';
6
6
  export { registerRun } from './run';
7
+ export { registerReload } from './reload';
7
8
  export { registerScreenshot } from './screenshot';
8
9
  export { registerSessionClose } from './session-close';
9
10
  export { registerSessionStart } from './session-start';
@@ -0,0 +1,56 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import type { McpRuntimeMode } from '../bootstrap';
4
+ import { err, text } from '../utility/response';
5
+ import { reloadSupportFiles } from '../utility/support';
6
+
7
+ type Options = {
8
+ runtimeMode: McpRuntimeMode;
9
+ };
10
+
11
+ async function resetBuiltInStepRegistry(): Promise<void> {
12
+ const bdd = (await import('@letsrunit/bdd')) as Record<string, unknown>;
13
+ const reset = bdd.resetRegistryToBuiltInSteps;
14
+ if (typeof reset !== 'function') {
15
+ throw new Error(
16
+ 'Installed @letsrunit/bdd does not expose resetRegistryToBuiltInSteps. Update @letsrunit/bdd to a compatible version.',
17
+ );
18
+ }
19
+ reset();
20
+ }
21
+
22
+ export function registerReload(server: McpServer, options: Options): void {
23
+ server.registerTool(
24
+ 'letsrunit_reload',
25
+ {
26
+ description:
27
+ 'Reload built-in and project support step definitions without restarting the MCP server.',
28
+ inputSchema: {
29
+ cwd: z
30
+ .string()
31
+ .optional()
32
+ .describe('Project directory to resolve cucumber support files from. Defaults to current project cwd.'),
33
+ },
34
+ },
35
+ async (input) => {
36
+ if (options.runtimeMode !== 'project') {
37
+ return err('Reload failed: letsrunit_reload is only available in project runtime mode.');
38
+ }
39
+
40
+ try {
41
+ await resetBuiltInStepRegistry();
42
+ const result = await reloadSupportFiles(input.cwd);
43
+ return text(
44
+ JSON.stringify({
45
+ reloaded: true,
46
+ projectRoot: result.projectRoot,
47
+ supportEntriesLoaded: result.supportEntriesLoaded,
48
+ ignoredEntries: result.ignoredEntries,
49
+ }),
50
+ );
51
+ } catch (e) {
52
+ return err(`Reload failed: ${(e as Error).message}`);
53
+ }
54
+ },
55
+ );
56
+ }
@@ -1,17 +1,27 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
+ import type { McpRuntimeMode } from '../bootstrap';
3
4
  import type { SessionManager } from '../sessions';
4
5
  import { err, text } from '../utility/response';
5
6
  import { loadSupportFiles } from '../utility/support';
6
7
 
7
- export function registerSessionStart(server: McpServer, sessions: SessionManager): void {
8
+ interface Options {
9
+ runtimeMode: McpRuntimeMode;
10
+ }
11
+
12
+ export function registerSessionStart(server: McpServer, sessions: SessionManager, opts?: Options): void {
8
13
  server.registerTool(
9
14
  'letsrunit_session_start',
10
15
  {
11
16
  description:
12
17
  'Launch a new browser session. Does not navigate anywhere — use letsrunit_run with a Given step to navigate. Set baseURL to enable relative paths like "Given I\'m on the homepage".',
13
18
  inputSchema: {
14
- baseURL: z.string().optional().describe('Base URL for the session, e.g. "http://localhost:3000". Enables relative paths in Given steps like "Given I\'m on the homepage" or "Given I\'m on page \\"/login\\""'),
19
+ baseURL: z
20
+ .string()
21
+ .optional()
22
+ .describe(
23
+ 'Base URL for the session, e.g. "http://localhost:3000". Enables relative paths in Given steps like "Given I\'m on the homepage" or "Given I\'m on page \\"/login\\""',
24
+ ),
15
25
  language: z.string().optional().describe("Browser language code, e.g. 'en', 'fr'"),
16
26
  headless: z.boolean().optional().describe('Run browser in headless mode (default: true)'),
17
27
  viewportWidth: z.number().int().optional().describe('Viewport width in pixels (default: 1280)'),
@@ -20,7 +30,7 @@ export function registerSessionStart(server: McpServer, sessions: SessionManager
20
30
  },
21
31
  async (input) => {
22
32
  try {
23
- if (process.env.LETSRUNIT_MCP_RUNTIME_MODE === 'project') {
33
+ if (opts?.runtimeMode === 'project') {
24
34
  await loadSupportFiles();
25
35
  }
26
36
 
@@ -0,0 +1,159 @@
1
+ import { loadConfiguration } from '@cucumber/cucumber/api';
2
+ import { registry } from '@letsrunit/bdd';
3
+ import { realpathSync } from 'node:fs';
4
+ import { createRequire } from 'node:module';
5
+ import { resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { decideHandoff, resolveRuntimeModeOverride } from '../bootstrap';
8
+ import {
9
+ expandPathPatterns,
10
+ findCucumberConfig,
11
+ getSupportLoadState,
12
+ loadLetsrunitIgnorePatterns,
13
+ resolveEffectiveCwd,
14
+ resolveSupportEntries,
15
+ type SupportEntry,
16
+ } from './support';
17
+
18
+ declare const __LETSRUNIT_VERSION__: string | undefined;
19
+
20
+ export type Diagnostics = {
21
+ envProjectCwd: string | null;
22
+ processCwd: string;
23
+ inputCwd: string | null;
24
+ effectiveCwd: string;
25
+ projectRoot: string;
26
+ cucumberConfigPath: string | null;
27
+ supportPatterns: string[];
28
+ ignorePatterns: string[];
29
+ ignoredPaths: string[];
30
+ supportEntries: SupportEntry[];
31
+ loadedProjectRoots: string[];
32
+ loadedSupportEntries: string[];
33
+ mcpServer: {
34
+ version: string;
35
+ executablePath: string | null;
36
+ projectServerUsed: boolean;
37
+ handoffDecision: {
38
+ shouldHandoff: boolean;
39
+ runtimeMode: string;
40
+ };
41
+ serverMcpPath: string | null;
42
+ projectMcpPath: string | null;
43
+ };
44
+ letsrunitEnv: Record<string, string>;
45
+ moduleResolution: {
46
+ serverBddPath: string | null;
47
+ projectBddPath: string | null;
48
+ };
49
+ registry: {
50
+ total: number;
51
+ byType: {
52
+ Given: number;
53
+ When: number;
54
+ Then: number;
55
+ };
56
+ definitions: Array<{
57
+ type: 'Given' | 'When' | 'Then';
58
+ source: string;
59
+ comment?: string;
60
+ }>;
61
+ };
62
+ };
63
+
64
+ function resolveFrom(moduleId: string, fromPath: string): string | null {
65
+ try {
66
+ const req = createRequire(fromPath);
67
+ return req.resolve(moduleId);
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ function toRealpath(path: string | null): string | null {
74
+ if (!path) return null;
75
+ try {
76
+ return realpathSync(path);
77
+ } catch {
78
+ return path;
79
+ }
80
+ }
81
+
82
+ function pickLetsrunitEnv(): Record<string, string> {
83
+ return Object.fromEntries(
84
+ Object.entries(process.env)
85
+ .filter(([key, value]) => key.startsWith('LETSRUNIT_') && typeof value === 'string')
86
+ .map(([key, value]) => [key, value as string]),
87
+ );
88
+ }
89
+
90
+ export async function collectDiagnostics(cwd?: string): Promise<Diagnostics> {
91
+ const effectiveCwd = resolveEffectiveCwd(cwd);
92
+ const projectRoot = resolve(effectiveCwd);
93
+ const cucumberConfigPath = findCucumberConfig(projectRoot);
94
+ const { useConfiguration } = await loadConfiguration({}, { cwd: projectRoot });
95
+ const supportPatterns = [...(useConfiguration.require ?? []), ...(useConfiguration.import ?? [])];
96
+ const ignorePatterns = await loadLetsrunitIgnorePatterns(projectRoot);
97
+ const ignoredPaths = await expandPathPatterns(projectRoot, ignorePatterns);
98
+ const supportEntries = await resolveSupportEntries(projectRoot, supportPatterns);
99
+ const supportLoadState = getSupportLoadState();
100
+ const serverBddPath = toRealpath(resolveFrom('@letsrunit/bdd', import.meta.url));
101
+ const projectBddPath = toRealpath(resolveFrom('@letsrunit/bdd', resolve(projectRoot, 'package.json')));
102
+ const projectMcpEntryPath = resolveFrom('@letsrunit/mcp-server', resolve(projectRoot, 'package.json'));
103
+ const currentEntrypointPath = toRealpath(fileURLToPath(import.meta.url));
104
+ const projectEntrypointPath = toRealpath(projectMcpEntryPath);
105
+ const handoffDecision = decideHandoff(
106
+ currentEntrypointPath,
107
+ projectEntrypointPath,
108
+ resolveRuntimeModeOverride(),
109
+ );
110
+ const serverMcpPath = toRealpath(resolveFrom('@letsrunit/mcp-server', import.meta.url));
111
+ const projectMcpPath = toRealpath(projectMcpEntryPath);
112
+ const executablePath = toRealpath(process.argv[1] ?? null);
113
+ const version = typeof __LETSRUNIT_VERSION__ === 'string' ? __LETSRUNIT_VERSION__ : 'unknown';
114
+ const registryDefinitions = registry.defs.map((def) => ({
115
+ type: def.type,
116
+ source: def.source,
117
+ comment: def.comment,
118
+ }));
119
+
120
+ return {
121
+ envProjectCwd: process.env.LETSRUNIT_PROJECT_CWD ?? null,
122
+ processCwd: process.cwd(),
123
+ inputCwd: cwd ?? null,
124
+ effectiveCwd,
125
+ projectRoot,
126
+ cucumberConfigPath,
127
+ supportPatterns,
128
+ ignorePatterns,
129
+ ignoredPaths: [...ignoredPaths].sort(),
130
+ supportEntries,
131
+ loadedProjectRoots: supportLoadState.loadedProjectRoots,
132
+ loadedSupportEntries: supportLoadState.loadedSupportEntries,
133
+ mcpServer: {
134
+ version,
135
+ executablePath,
136
+ projectServerUsed: handoffDecision.runtimeMode === 'project',
137
+ handoffDecision: {
138
+ shouldHandoff: handoffDecision.shouldHandoff,
139
+ runtimeMode: handoffDecision.runtimeMode,
140
+ },
141
+ serverMcpPath,
142
+ projectMcpPath,
143
+ },
144
+ letsrunitEnv: pickLetsrunitEnv(),
145
+ moduleResolution: {
146
+ serverBddPath,
147
+ projectBddPath,
148
+ },
149
+ registry: {
150
+ total: registryDefinitions.length,
151
+ byType: {
152
+ Given: registryDefinitions.filter((d) => d.type === 'Given').length,
153
+ When: registryDefinitions.filter((d) => d.type === 'When').length,
154
+ Then: registryDefinitions.filter((d) => d.type === 'Then').length,
155
+ },
156
+ definitions: registryDefinitions,
157
+ },
158
+ };
159
+ }
@@ -1,13 +1,8 @@
1
1
  import { loadConfiguration } from '@cucumber/cucumber/api';
2
- import { registry } from '@letsrunit/bdd';
3
- import { existsSync, realpathSync } from 'node:fs';
2
+ import { existsSync } from 'node:fs';
4
3
  import { glob } from 'node:fs/promises';
5
- import { createRequire } from 'node:module';
6
4
  import { isAbsolute, resolve } from 'node:path';
7
- import { fileURLToPath, pathToFileURL } from 'node:url';
8
- import { decideHandoff } from '../bootstrap';
9
-
10
- declare const __LETSRUNIT_VERSION__: string;
5
+ import { pathToFileURL } from 'node:url';
11
6
 
12
7
  type CucumberConfig = {
13
8
  require?: unknown;
@@ -17,7 +12,7 @@ type CucumberConfig = {
17
12
  };
18
13
  };
19
14
 
20
- type SupportEntry = { kind: 'path'; value: string } | { kind: 'module'; value: string };
15
+ export type SupportEntry = { kind: 'path'; value: string } | { kind: 'module'; value: string };
21
16
 
22
17
  const CUCUMBER_CONFIG_FILES = [
23
18
  'cucumber.js',
@@ -31,48 +26,14 @@ const CUCUMBER_CONFIG_FILES = [
31
26
  const loadedProjectRoots = new Set<string>();
32
27
  const loadedSupportEntries = new Set<string>();
33
28
 
34
- export type SupportDiagnostics = {
35
- envProjectCwd: string | null;
36
- processCwd: string;
37
- inputCwd: string | null;
38
- effectiveCwd: string;
29
+ export type SupportLoadResult = {
39
30
  projectRoot: string;
40
- cucumberConfigPath: string | null;
41
- supportPatterns: string[];
42
- ignorePatterns: string[];
43
- ignoredPaths: string[];
44
- supportEntries: SupportEntry[];
45
- loadedProjectRoots: string[];
46
- loadedSupportEntries: string[];
47
- mcpServer: {
48
- version: string;
49
- executablePath: string | null;
50
- projectServerUsed: boolean;
51
- handoffDecision: {
52
- shouldHandoff: boolean;
53
- runtimeMode: string;
54
- };
55
- serverMcpPath: string | null;
56
- projectMcpPath: string | null;
57
- };
58
- letsrunitEnv: Record<string, string>;
59
- moduleResolution: {
60
- serverBddPath: string | null;
61
- projectBddPath: string | null;
62
- };
63
- registry: {
64
- total: number;
65
- byType: {
66
- Given: number;
67
- When: number;
68
- Then: number;
69
- };
70
- definitions: Array<{
71
- type: 'Given' | 'When' | 'Then';
72
- source: string;
73
- comment?: string;
74
- }>;
75
- };
31
+ supportEntriesLoaded: number;
32
+ ignoredEntries: number;
33
+ };
34
+
35
+ type LoadSupportOptions = {
36
+ forceReload?: boolean;
76
37
  };
77
38
 
78
39
  function toStrings(value: unknown): string[] {
@@ -96,7 +57,7 @@ function normalizeMatch(baseDir: string, match: string): string {
96
57
  return isAbsolute(match) ? resolve(match) : resolve(baseDir, match);
97
58
  }
98
59
 
99
- async function expandPathPatterns(baseDir: string, patterns: string[]): Promise<Set<string>> {
60
+ export async function expandPathPatterns(baseDir: string, patterns: string[]): Promise<Set<string>> {
100
61
  const files = new Set<string>();
101
62
 
102
63
  for (const pattern of patterns) {
@@ -113,7 +74,7 @@ async function expandPathPatterns(baseDir: string, patterns: string[]): Promise<
113
74
  return files;
114
75
  }
115
76
 
116
- async function resolveSupportEntries(baseDir: string, entries: string[]): Promise<SupportEntry[]> {
77
+ export async function resolveSupportEntries(baseDir: string, entries: string[]): Promise<SupportEntry[]> {
117
78
  const resolved: SupportEntry[] = [];
118
79
 
119
80
  for (const entry of entries) {
@@ -135,7 +96,7 @@ async function resolveSupportEntries(baseDir: string, entries: string[]): Promis
135
96
  return resolved;
136
97
  }
137
98
 
138
- function findCucumberConfig(cwd: string): string | null {
99
+ export function findCucumberConfig(cwd: string): string | null {
139
100
  for (const filename of CUCUMBER_CONFIG_FILES) {
140
101
  const path = resolve(cwd, filename);
141
102
  if (existsSync(path)) return path;
@@ -144,7 +105,7 @@ function findCucumberConfig(cwd: string): string | null {
144
105
  return null;
145
106
  }
146
107
 
147
- async function loadLetsrunitIgnorePatterns(cwd: string): Promise<string[]> {
108
+ export async function loadLetsrunitIgnorePatterns(cwd: string): Promise<string[]> {
148
109
  const configPath = findCucumberConfig(cwd);
149
110
  if (!configPath) return [];
150
111
 
@@ -154,141 +115,73 @@ async function loadLetsrunitIgnorePatterns(cwd: string): Promise<string[]> {
154
115
  return toStrings(config.letsrunit?.ignore);
155
116
  }
156
117
 
157
- function resolveEffectiveCwd(cwd?: string): string {
118
+ export function resolveEffectiveCwd(cwd?: string): string {
158
119
  return cwd ?? process.env.LETSRUNIT_PROJECT_CWD ?? process.cwd();
159
120
  }
160
121
 
161
- function resolveFrom(moduleId: string, fromPath: string): string | null {
162
- try {
163
- const req = createRequire(fromPath);
164
- return req.resolve(moduleId);
165
- } catch {
166
- return null;
167
- }
168
- }
169
-
170
- function toRealpath(path: string | null): string | null {
171
- if (!path) return null;
172
- try {
173
- return realpathSync(path);
174
- } catch {
175
- return path;
176
- }
177
- }
178
-
179
- function pickLetsrunitEnv(): Record<string, string> {
180
- return Object.fromEntries(
181
- Object.entries(process.env)
182
- .filter(([key, value]) => key.startsWith('LETSRUNIT_') && typeof value === 'string')
183
- .map(([key, value]) => [key, value as string]),
184
- );
185
- }
186
-
187
- function resolveMcpServerVersion(): string {
188
- return typeof __LETSRUNIT_VERSION__ === 'string' ? __LETSRUNIT_VERSION__ : 'unknown';
189
- }
190
-
191
- export async function collectSupportDiagnostics(cwd?: string): Promise<SupportDiagnostics> {
192
- const effectiveCwd = resolveEffectiveCwd(cwd);
193
- const projectRoot = resolve(effectiveCwd);
194
- const cucumberConfigPath = findCucumberConfig(projectRoot);
195
- const { useConfiguration } = await loadConfiguration({}, { cwd: projectRoot });
196
- const supportPatterns = [...toStrings(useConfiguration.require), ...toStrings(useConfiguration.import)];
197
- const ignorePatterns = await loadLetsrunitIgnorePatterns(projectRoot);
198
- const ignoredPaths = await expandPathPatterns(projectRoot, ignorePatterns);
199
- const supportEntries = await resolveSupportEntries(projectRoot, supportPatterns);
200
- const serverBddPath = toRealpath(resolveFrom('@letsrunit/bdd', import.meta.url));
201
- const projectBddPath = toRealpath(resolveFrom('@letsrunit/bdd', resolve(projectRoot, 'package.json')));
202
- const projectMcpEntryPath = resolveFrom('@letsrunit/mcp-server', resolve(projectRoot, 'package.json'));
203
- const currentEntrypointPath = toRealpath(fileURLToPath(import.meta.url));
204
- const projectEntrypointPath = toRealpath(projectMcpEntryPath);
205
- const handoffDecision = decideHandoff(
206
- currentEntrypointPath,
207
- projectEntrypointPath,
208
- process.env.LETSRUNIT_MCP_BOOTSTRAPPED === '1',
209
- );
210
- const serverMcpPath = toRealpath(resolveFrom('@letsrunit/mcp-server', import.meta.url));
211
- const projectMcpPath = toRealpath(projectMcpEntryPath);
212
- const executablePath = toRealpath(process.argv[1] ?? null);
213
- const version = resolveMcpServerVersion();
214
- const registryDefinitions = registry.defs.map((def) => ({
215
- type: def.type,
216
- source: def.source,
217
- comment: def.comment,
218
- }));
219
-
122
+ export function getSupportLoadState(): { loadedProjectRoots: string[]; loadedSupportEntries: string[] } {
220
123
  return {
221
- envProjectCwd: process.env.LETSRUNIT_PROJECT_CWD ?? null,
222
- processCwd: process.cwd(),
223
- inputCwd: cwd ?? null,
224
- effectiveCwd,
225
- projectRoot,
226
- cucumberConfigPath,
227
- supportPatterns,
228
- ignorePatterns,
229
- ignoredPaths: [...ignoredPaths].sort(),
230
- supportEntries,
231
124
  loadedProjectRoots: [...loadedProjectRoots].sort(),
232
125
  loadedSupportEntries: [...loadedSupportEntries].sort(),
233
- mcpServer: {
234
- version,
235
- executablePath,
236
- projectServerUsed: handoffDecision.runtimeMode === 'project',
237
- handoffDecision: {
238
- shouldHandoff: handoffDecision.shouldHandoff,
239
- runtimeMode: handoffDecision.runtimeMode,
240
- },
241
- serverMcpPath,
242
- projectMcpPath,
243
- },
244
- letsrunitEnv: pickLetsrunitEnv(),
245
- moduleResolution: {
246
- serverBddPath,
247
- projectBddPath,
248
- },
249
- registry: {
250
- total: registryDefinitions.length,
251
- byType: {
252
- Given: registryDefinitions.filter((d) => d.type === 'Given').length,
253
- When: registryDefinitions.filter((d) => d.type === 'When').length,
254
- Then: registryDefinitions.filter((d) => d.type === 'Then').length,
255
- },
256
- definitions: registryDefinitions,
257
- },
258
126
  };
259
127
  }
260
128
 
261
- export async function loadSupportFiles(cwd?: string): Promise<void> {
129
+ export function clearSupportLoadState(): void {
130
+ loadedProjectRoots.clear();
131
+ loadedSupportEntries.clear();
132
+ }
133
+
134
+ function buildReloadedFileUrl(path: string): string {
135
+ const url = pathToFileURL(path);
136
+ url.searchParams.set('letsrunitReload', Date.now().toString(36));
137
+ return url.href;
138
+ }
139
+
140
+ export async function loadSupportFiles(cwd?: string, options?: LoadSupportOptions): Promise<SupportLoadResult> {
262
141
  const projectRoot = resolve(resolveEffectiveCwd(cwd));
263
- if (loadedProjectRoots.has(projectRoot)) return;
142
+ const forceReload = options?.forceReload === true;
143
+
144
+ if (!forceReload && loadedProjectRoots.has(projectRoot)) {
145
+ return { projectRoot, supportEntriesLoaded: 0, ignoredEntries: 0 };
146
+ }
264
147
 
265
148
  const { useConfiguration } = await loadConfiguration({}, { cwd: projectRoot });
266
149
  const supportPatterns = [...toStrings(useConfiguration.require), ...toStrings(useConfiguration.import)];
267
150
  if (supportPatterns.length === 0) {
268
151
  loadedProjectRoots.add(projectRoot);
269
- return;
152
+ return { projectRoot, supportEntriesLoaded: 0, ignoredEntries: 0 };
270
153
  }
271
154
 
272
155
  const ignorePatterns = await loadLetsrunitIgnorePatterns(projectRoot);
273
156
  const ignoredPaths = await expandPathPatterns(projectRoot, ignorePatterns);
274
157
  const supportEntries = await resolveSupportEntries(projectRoot, supportPatterns);
158
+ let supportEntriesLoaded = 0;
159
+ let ignoredEntries = 0;
275
160
 
276
161
  for (const entry of supportEntries) {
277
162
  if (entry.kind === 'path' && ignoredPaths.has(entry.value)) {
163
+ ignoredEntries += 1;
278
164
  continue;
279
165
  }
280
166
 
281
167
  const key = `${entry.kind}:${entry.value}`;
282
- if (loadedSupportEntries.has(key)) continue;
168
+ if (!forceReload && loadedSupportEntries.has(key)) continue;
283
169
 
284
170
  if (entry.kind === 'path') {
285
- await import(pathToFileURL(entry.value).href);
171
+ await import(forceReload ? buildReloadedFileUrl(entry.value) : pathToFileURL(entry.value).href);
286
172
  } else {
287
173
  await import(entry.value);
288
174
  }
289
175
 
176
+ supportEntriesLoaded += 1;
290
177
  loadedSupportEntries.add(key);
291
178
  }
292
179
 
293
180
  loadedProjectRoots.add(projectRoot);
181
+ return { projectRoot, supportEntriesLoaded, ignoredEntries };
182
+ }
183
+
184
+ export async function reloadSupportFiles(cwd?: string): Promise<SupportLoadResult> {
185
+ clearSupportLoadState();
186
+ return loadSupportFiles(cwd, { forceReload: true });
294
187
  }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/bootstrap.ts"],"names":[],"mappings":"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","file":"chunk-XJCBPTOU.js","sourcesContent":["import { spawnSync } from 'node:child_process';\nimport { realpathSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nexport type McpRuntimeMode = 'project' | 'standalone';\n\nexport type HandoffDecision = {\n shouldHandoff: boolean;\n runtimeMode: McpRuntimeMode;\n};\n\nfunction resolveProjectRoot(): string {\n return resolve(process.env.LETSRUNIT_PROJECT_CWD ?? process.cwd());\n}\n\nfunction resolveFromProject(moduleId: string, projectRoot: string): string | null {\n try {\n const req = createRequire(resolve(projectRoot, 'package.json'));\n return req.resolve(moduleId);\n } catch {\n return null;\n }\n}\n\nfunction toRealpath(path: string | null): string | null {\n if (!path) return null;\n try {\n return realpathSync(path);\n } catch {\n return null;\n }\n}\n\nfunction sameEntrypoint(a: string | null, b: string | null): boolean {\n if (!a || !b) return false;\n return a === b;\n}\n\nexport function decideHandoff(\n currentEntrypointPath: string | null,\n projectEntrypointPath: string | null,\n isBootstrapped: boolean,\n): HandoffDecision {\n if (!projectEntrypointPath) {\n return { shouldHandoff: false, runtimeMode: 'standalone' };\n }\n\n if (sameEntrypoint(currentEntrypointPath, projectEntrypointPath)) {\n return { shouldHandoff: false, runtimeMode: 'project' };\n }\n\n if (isBootstrapped) {\n return { shouldHandoff: false, runtimeMode: 'standalone' };\n }\n\n return { shouldHandoff: true, runtimeMode: 'project' };\n}\n\nfunction runProjectLocalServer(projectEntrypointPath: string): never {\n const result = spawnSync(process.execPath, [projectEntrypointPath, ...process.argv.slice(2)], {\n stdio: 'inherit',\n env: {\n ...process.env,\n LETSRUNIT_MCP_BOOTSTRAPPED: '1',\n LETSRUNIT_MCP_RUNTIME_MODE: 'project',\n },\n });\n\n if (result.error) throw result.error;\n process.exit(result.status ?? 1);\n}\n\nexport function bootstrapProjectServer(): McpRuntimeMode {\n const projectRoot = resolveProjectRoot();\n const isBootstrapped = process.env.LETSRUNIT_MCP_BOOTSTRAPPED === '1';\n\n const currentEntryPath = toRealpath(fileURLToPath(import.meta.url));\n const projectEntryPath = toRealpath(resolveFromProject('@letsrunit/mcp-server', projectRoot));\n\n const decision = decideHandoff(currentEntryPath, projectEntryPath, isBootstrapped);\n\n if (decision.shouldHandoff && projectEntryPath) {\n runProjectLocalServer(projectEntryPath);\n }\n\n process.env.LETSRUNIT_MCP_RUNTIME_MODE = decision.runtimeMode;\n return decision.runtimeMode;\n}\n"]}