@letsrunit/mcp-server 0.13.3 → 0.14.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.
@@ -0,0 +1,115 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { readFileSync, realpathSync } from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import { dirname, resolve } from 'node:path';
5
+
6
+ export type McpRuntimeMode = 'project' | 'standalone';
7
+
8
+ export type HandoffDecision = {
9
+ shouldHandoff: boolean;
10
+ runtimeMode: McpRuntimeMode;
11
+ };
12
+
13
+ type PackageJsonLike = {
14
+ bin?: string | Record<string, string>;
15
+ };
16
+
17
+ function resolveProjectRoot(): string {
18
+ return resolve(process.env.LETSRUNIT_PROJECT_CWD ?? process.cwd());
19
+ }
20
+
21
+ function resolveFromProject(moduleId: string, projectRoot: string): string | null {
22
+ try {
23
+ const req = createRequire(resolve(projectRoot, 'package.json'));
24
+ return req.resolve(moduleId);
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ function toRealpath(path: string | null): string | null {
31
+ if (!path) return null;
32
+ try {
33
+ return realpathSync(path);
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ function samePackageRoot(a: string | null, b: string | null): boolean {
40
+ if (!a || !b) return false;
41
+ return dirname(a) === dirname(b);
42
+ }
43
+
44
+ function resolveBinPath(packageJsonPath: string): string {
45
+ const text = readFileSync(packageJsonPath, 'utf8');
46
+ const pkg = JSON.parse(text) as PackageJsonLike;
47
+
48
+ if (typeof pkg.bin === 'string') {
49
+ return resolve(dirname(packageJsonPath), pkg.bin);
50
+ }
51
+
52
+ if (pkg.bin && typeof pkg.bin === 'object') {
53
+ const named = pkg.bin['letsrunit-mcp'];
54
+ const first = Object.values(pkg.bin)[0];
55
+ const bin = named ?? first;
56
+ if (typeof bin === 'string') {
57
+ return resolve(dirname(packageJsonPath), bin);
58
+ }
59
+ }
60
+
61
+ throw new Error(`Unable to resolve mcp bin from ${packageJsonPath}`);
62
+ }
63
+
64
+ export function decideHandoff(
65
+ currentPackageJsonPath: string | null,
66
+ projectPackageJsonPath: string | null,
67
+ isBootstrapped: boolean,
68
+ ): HandoffDecision {
69
+ if (!projectPackageJsonPath) {
70
+ return { shouldHandoff: false, runtimeMode: 'standalone' };
71
+ }
72
+
73
+ if (samePackageRoot(currentPackageJsonPath, projectPackageJsonPath)) {
74
+ return { shouldHandoff: false, runtimeMode: 'project' };
75
+ }
76
+
77
+ if (isBootstrapped) {
78
+ return { shouldHandoff: false, runtimeMode: 'standalone' };
79
+ }
80
+
81
+ return { shouldHandoff: true, runtimeMode: 'project' };
82
+ }
83
+
84
+ function runProjectLocalServer(projectPackageJsonPath: string): never {
85
+ const entry = resolveBinPath(projectPackageJsonPath);
86
+ const result = spawnSync(process.execPath, [entry, ...process.argv.slice(2)], {
87
+ stdio: 'inherit',
88
+ env: {
89
+ ...process.env,
90
+ LETSRUNIT_MCP_BOOTSTRAPPED: '1',
91
+ LETSRUNIT_MCP_RUNTIME_MODE: 'project',
92
+ },
93
+ });
94
+
95
+ if (result.error) throw result.error;
96
+ process.exit(result.status ?? 1);
97
+ }
98
+
99
+ export function bootstrapProjectServer(): McpRuntimeMode {
100
+ const projectRoot = resolveProjectRoot();
101
+ const isBootstrapped = process.env.LETSRUNIT_MCP_BOOTSTRAPPED === '1';
102
+ const currentReq = createRequire(import.meta.url);
103
+
104
+ const currentPackageJsonPath = toRealpath(currentReq.resolve('../package.json'));
105
+ const projectPackageJsonPath = toRealpath(resolveFromProject('@letsrunit/mcp-server/package.json', projectRoot));
106
+
107
+ const decision = decideHandoff(currentPackageJsonPath, projectPackageJsonPath, isBootstrapped);
108
+
109
+ if (decision.shouldHandoff && projectPackageJsonPath) {
110
+ runProjectLocalServer(projectPackageJsonPath);
111
+ }
112
+
113
+ process.env.LETSRUNIT_MCP_RUNTIME_MODE = decision.runtimeMode;
114
+ return decision.runtimeMode;
115
+ }
package/src/index.ts CHANGED
@@ -1,10 +1,13 @@
1
1
  import { createRequire } from 'module';
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { SessionManager } from './sessions';
4
+ import { bootstrapProjectServer } from './bootstrap';
5
5
 
6
6
  const { version } = createRequire(import.meta.url)('../package.json') as { version: string };
7
- import {
7
+ bootstrapProjectServer();
8
+
9
+ const { SessionManager } = await import('./sessions');
10
+ const {
8
11
  registerDebug,
9
12
  registerDiagnostics,
10
13
  registerDiff,
@@ -15,7 +18,7 @@ import {
15
18
  registerSessionClose,
16
19
  registerSessionStart,
17
20
  registerSnapshot,
18
- } from './tools';
21
+ } = await import('./tools');
19
22
 
20
23
  const sessions = new SessionManager();
21
24
 
@@ -20,7 +20,9 @@ export function registerSessionStart(server: McpServer, sessions: SessionManager
20
20
  },
21
21
  async (input) => {
22
22
  try {
23
- await loadSupportFiles();
23
+ if (process.env.LETSRUNIT_MCP_RUNTIME_MODE === 'project') {
24
+ await loadSupportFiles();
25
+ }
24
26
 
25
27
  const viewport =
26
28
  input.viewportWidth || input.viewportHeight