frontmcp 1.2.1 → 1.3.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.
Files changed (108) hide show
  1. package/package.json +4 -4
  2. package/src/commands/build/exec/bin-meta.d.ts +49 -0
  3. package/src/commands/build/exec/bin-meta.js +68 -0
  4. package/src/commands/build/exec/bin-meta.js.map +1 -0
  5. package/src/commands/build/exec/cli-runtime/generate-cli-entry.js +195 -3
  6. package/src/commands/build/exec/cli-runtime/generate-cli-entry.js.map +1 -1
  7. package/src/commands/build/exec/cli-runtime/plugin-emitter.d.ts +160 -0
  8. package/src/commands/build/exec/cli-runtime/plugin-emitter.js +512 -0
  9. package/src/commands/build/exec/cli-runtime/plugin-emitter.js.map +1 -0
  10. package/src/commands/build/exec/cli-runtime/schema-extractor.d.ts +13 -1
  11. package/src/commands/build/exec/cli-runtime/schema-extractor.js +29 -3
  12. package/src/commands/build/exec/cli-runtime/schema-extractor.js.map +1 -1
  13. package/src/commands/build/exec/cli-runtime/skill-md-compose.d.ts +25 -0
  14. package/src/commands/build/exec/cli-runtime/skill-md-compose.js +63 -0
  15. package/src/commands/build/exec/cli-runtime/skill-md-compose.js.map +1 -0
  16. package/src/commands/build/exec/index.js +26 -0
  17. package/src/commands/build/exec/index.js.map +1 -1
  18. package/src/commands/dev/bridge/child-supervisor.d.ts +48 -0
  19. package/src/commands/dev/bridge/child-supervisor.js +228 -0
  20. package/src/commands/dev/bridge/child-supervisor.js.map +1 -0
  21. package/src/commands/dev/bridge/errors.d.ts +23 -0
  22. package/src/commands/dev/bridge/errors.js +34 -0
  23. package/src/commands/dev/bridge/errors.js.map +1 -0
  24. package/src/commands/dev/bridge/index.d.ts +30 -0
  25. package/src/commands/dev/bridge/index.js +220 -0
  26. package/src/commands/dev/bridge/index.js.map +1 -0
  27. package/src/commands/dev/bridge/log.d.ts +29 -0
  28. package/src/commands/dev/bridge/log.js +82 -0
  29. package/src/commands/dev/bridge/log.js.map +1 -0
  30. package/src/commands/dev/bridge/state-machine.d.ts +56 -0
  31. package/src/commands/dev/bridge/state-machine.js +245 -0
  32. package/src/commands/dev/bridge/state-machine.js.map +1 -0
  33. package/src/commands/dev/bridge/stdio-framer.d.ts +47 -0
  34. package/src/commands/dev/bridge/stdio-framer.js +128 -0
  35. package/src/commands/dev/bridge/stdio-framer.js.map +1 -0
  36. package/src/commands/dev/bridge/upstream-client.d.ts +49 -0
  37. package/src/commands/dev/bridge/upstream-client.js +159 -0
  38. package/src/commands/dev/bridge/upstream-client.js.map +1 -0
  39. package/src/commands/dev/bridge/watcher.d.ts +30 -0
  40. package/src/commands/dev/bridge/watcher.js +87 -0
  41. package/src/commands/dev/bridge/watcher.js.map +1 -0
  42. package/src/commands/dev/dev.d.ts +18 -1
  43. package/src/commands/dev/dev.js +134 -14
  44. package/src/commands/dev/dev.js.map +1 -1
  45. package/src/commands/dev/inspector.d.ts +13 -1
  46. package/src/commands/dev/inspector.js +77 -3
  47. package/src/commands/dev/inspector.js.map +1 -1
  48. package/src/commands/dev/port.d.ts +23 -0
  49. package/src/commands/dev/port.js +87 -0
  50. package/src/commands/dev/port.js.map +1 -0
  51. package/src/commands/dev/register.d.ts +1 -1
  52. package/src/commands/dev/register.js +28 -4
  53. package/src/commands/dev/register.js.map +1 -1
  54. package/src/commands/dev/test.d.ts +26 -1
  55. package/src/commands/dev/test.js +181 -64
  56. package/src/commands/dev/test.js.map +1 -1
  57. package/src/commands/eject/mcp-client.d.ts +25 -0
  58. package/src/commands/eject/mcp-client.js +74 -0
  59. package/src/commands/eject/mcp-client.js.map +1 -0
  60. package/src/commands/eject/register.d.ts +9 -0
  61. package/src/commands/eject/register.js +56 -0
  62. package/src/commands/eject/register.js.map +1 -0
  63. package/src/commands/install/install-claude-plugin.d.ts +13 -0
  64. package/src/commands/install/install-claude-plugin.js +327 -0
  65. package/src/commands/install/install-claude-plugin.js.map +1 -0
  66. package/src/commands/install/register.d.ts +16 -0
  67. package/src/commands/install/register.js +70 -0
  68. package/src/commands/install/register.js.map +1 -0
  69. package/src/commands/scaffold/create.js +44 -0
  70. package/src/commands/scaffold/create.js.map +1 -1
  71. package/src/commands/skills/from-entry.d.ts +31 -0
  72. package/src/commands/skills/from-entry.js +68 -0
  73. package/src/commands/skills/from-entry.js.map +1 -0
  74. package/src/commands/skills/install.d.ts +12 -0
  75. package/src/commands/skills/install.js +173 -8
  76. package/src/commands/skills/install.js.map +1 -1
  77. package/src/commands/skills/register.js +7 -3
  78. package/src/commands/skills/register.js.map +1 -1
  79. package/src/config/frontmcp-config.loader.d.ts +28 -0
  80. package/src/config/frontmcp-config.loader.js +146 -67
  81. package/src/config/frontmcp-config.loader.js.map +1 -1
  82. package/src/config/frontmcp-config.resolve.d.ts +67 -0
  83. package/src/config/frontmcp-config.resolve.js +118 -0
  84. package/src/config/frontmcp-config.resolve.js.map +1 -0
  85. package/src/config/frontmcp-config.schema.d.ts +207 -0
  86. package/src/config/frontmcp-config.schema.js +217 -1
  87. package/src/config/frontmcp-config.schema.js.map +1 -1
  88. package/src/config/frontmcp-config.types.d.ts +133 -0
  89. package/src/config/frontmcp-config.types.js.map +1 -1
  90. package/src/config/index.d.ts +2 -1
  91. package/src/config/index.js +3 -1
  92. package/src/config/index.js.map +1 -1
  93. package/src/core/args.d.ts +13 -0
  94. package/src/core/args.js.map +1 -1
  95. package/src/core/bridge.js +39 -0
  96. package/src/core/bridge.js.map +1 -1
  97. package/src/core/cli.d.ts +0 -6
  98. package/src/core/cli.js +23 -3
  99. package/src/core/cli.js.map +1 -1
  100. package/src/core/help.d.ts +1 -1
  101. package/src/core/help.js +27 -6
  102. package/src/core/help.js.map +1 -1
  103. package/src/core/program.d.ts +1 -1
  104. package/src/core/program.js +56 -12
  105. package/src/core/program.js.map +1 -1
  106. package/src/core/project-commands.d.ts +44 -0
  107. package/src/core/project-commands.js +216 -0
  108. package/src/core/project-commands.js.map +1 -0
@@ -1,12 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveDevPort = resolveDevPort;
3
4
  exports.runDev = runDev;
4
5
  const tslib_1 = require("tslib");
5
- const path = tslib_1.__importStar(require("path"));
6
6
  const child_process_1 = require("child_process");
7
+ const path = tslib_1.__importStar(require("path"));
8
+ const config_1 = require("../../config");
7
9
  const colors_1 = require("../../core/colors");
8
- const fs_1 = require("../../shared/fs");
9
10
  const env_1 = require("../../shared/env");
11
+ const fs_1 = require("../../shared/fs");
12
+ const port_1 = require("./port");
13
+ const DEFAULT_DEV_PORT = 3000;
10
14
  function killQuiet(proc, signal = 'SIGINT') {
11
15
  try {
12
16
  if (proc && proc.exitCode === null && proc.signalCode === null) {
@@ -17,26 +21,132 @@ function killQuiet(proc, signal = 'SIGINT') {
17
21
  // ignore
18
22
  }
19
23
  }
24
+ /**
25
+ * Resolve the port the dev child should bind to and report any conflict
26
+ * clearly. Returns the chosen port — or never returns and exits the process
27
+ * with a clear error when the port is busy and `--auto-port` was not set.
28
+ *
29
+ * Issue #398: previously the child crashed with a raw `EADDRINUSE` stack
30
+ * trace; this helper turns that into a one-line message with a suggested
31
+ * remediation and (optionally) the owning process.
32
+ */
33
+ async function resolveDevPort(opts) {
34
+ const exit = opts.exit ?? ((code) => process.exit(code));
35
+ const log = opts.log ?? ((msg) => console.error(msg));
36
+ const explicit = opts.port ?? (opts.envPort !== undefined && opts.envPort !== '' ? Number(opts.envPort) : undefined);
37
+ const port = explicit !== undefined && Number.isFinite(explicit) && explicit > 0
38
+ ? explicit
39
+ : DEFAULT_DEV_PORT;
40
+ if (await (0, port_1.isPortFree)(port))
41
+ return port;
42
+ if (opts.autoPort) {
43
+ const alt = await (0, port_1.findNextFreePort)(port + 1);
44
+ log(`${(0, colors_1.c)('yellow', '[dev]')} port ${port} is in use; auto-picked ${alt}`);
45
+ return alt;
46
+ }
47
+ // Build a clear, actionable error message.
48
+ const lines = [
49
+ `${(0, colors_1.c)('red', '[dev]')} Port ${port} is already in use — refusing to start.`,
50
+ `${(0, colors_1.c)('gray', ' ')} Retry with one of:`,
51
+ `${(0, colors_1.c)('gray', ' ')} • ${(0, colors_1.c)('bold', `frontmcp dev --port <other-port>`)}`,
52
+ `${(0, colors_1.c)('gray', ' ')} • ${(0, colors_1.c)('bold', `frontmcp dev --auto-port`)} ${(0, colors_1.c)('gray', '(pick the next free port automatically)')}`,
53
+ `${(0, colors_1.c)('gray', ' ')} • ${(0, colors_1.c)('bold', `PORT=<other-port> frontmcp dev`)}`,
54
+ ];
55
+ if (opts.showConflict) {
56
+ const owner = await (0, port_1.lookupPortOwner)(port);
57
+ if (owner) {
58
+ lines.push(`${(0, colors_1.c)('gray', ' ')} Holder of ${port}:`);
59
+ for (const row of owner.split('\n'))
60
+ lines.push(`${(0, colors_1.c)('gray', ' ')} ${row}`);
61
+ }
62
+ else {
63
+ lines.push(`${(0, colors_1.c)('gray', ' ')} (could not identify the holder of port ${port})`);
64
+ }
65
+ }
66
+ else {
67
+ lines.push(`${(0, colors_1.c)('gray', ' ')} (pass --show-conflict to print which process is holding the port)`);
68
+ }
69
+ for (const line of lines)
70
+ log(line);
71
+ return exit(1);
72
+ }
20
73
  async function runDev(opts) {
74
+ // Issue #399 — `--stdio` runs the first-party watch-aware stdio bridge
75
+ // instead of the legacy `tsx --watch + tsc --noEmit --watch` pair. The
76
+ // bridge owns process stdin/stdout (JSON-RPC frames only), holds the
77
+ // upstream MCP session across child restarts, and replaces the
78
+ // third-party `mcp-remote` recipe for the dev loop.
79
+ if (opts.stdio) {
80
+ const { runDevBridge } = await import('./bridge/index.js');
81
+ return runDevBridge(opts);
82
+ }
21
83
  const cwd = process.cwd();
22
- const entry = await (0, fs_1.resolveEntry)(cwd, opts.entry);
23
- // Load .env and .env.local files before starting the server
84
+ // Issue #400 resolve frontmcp.config so `entry`, `transport.http.port`,
85
+ // and `env.shared`/`env.dev` overlays apply. Precedence:
86
+ // CLI flag > FRONTMCP_<NAME> env > frontmcp.config field > built-in default.
87
+ const resolved = await (0, config_1.resolveConfig)({
88
+ cwd,
89
+ mode: 'dev',
90
+ configPath: typeof opts.config === 'string' ? opts.config : undefined,
91
+ });
92
+ const cfg = resolved.config;
93
+ const cliEntry = typeof opts.entry === 'string' ? opts.entry : undefined;
94
+ const configEntry = typeof cfg?.entry === 'string' ? cfg.entry : undefined;
95
+ const entry = await (0, fs_1.resolveEntry)(cwd, cliEntry ?? configEntry);
96
+ // Load .env and .env.local files (these win over config env overlays for
97
+ // parity with existing behavior — file-based env is the deployment escape
98
+ // hatch and shouldn't be silently overridden by committed config).
24
99
  (0, env_1.loadDevEnv)(cwd);
100
+ // Resolve the port BEFORE spawning tsx so EADDRINUSE produces a clean
101
+ // one-line error instead of a raw node:net stack trace (issue #398).
102
+ //
103
+ // Two caveats worth knowing about this pre-flight check:
104
+ // 1. TOCTOU — between this probe returning and the child actually binding,
105
+ // another process can grab the port. We accept that race: this is a
106
+ // dev-time tool, the worst case reverts to the prior behaviour (the
107
+ // child surfaces a raw EADDRINUSE), and the common case (port already
108
+ // busy at startup) is the one we wanted to fix.
109
+ // 2. The resolved port is exported as `PORT` to the child. It only takes
110
+ // effect when the user's `@FrontMcp({ http: { port } })` reads
111
+ // `process.env.PORT` (the SDK's `httpOptionsSchema` default does).
112
+ // If the user's metadata HARD-CODES `http.port`, the child binds to
113
+ // that hard-coded value and ignores PORT — the probe is then advisory
114
+ // only. Documented in docs/frontmcp/deployment/local-dev-server.mdx.
115
+ const cliPort = typeof opts.port === 'number' ? opts.port : opts.port ? Number(opts.port) : undefined;
116
+ const configPort = cfg?.transport?.http?.port;
117
+ const port = await resolveDevPort({
118
+ port: cliPort ?? configPort,
119
+ autoPort: !!opts.autoPort,
120
+ showConflict: !!opts.showConflict,
121
+ envPort: process.env['PORT'],
122
+ });
25
123
  console.log(`${(0, colors_1.c)('cyan', '[dev]')} using entry: ${path.relative(cwd, entry)}`);
124
+ if (resolved.configPath || resolved.configDir) {
125
+ console.log(`${(0, colors_1.c)('gray', '[dev]')} config: ${resolved.configPath ?? resolved.configDir}`);
126
+ }
127
+ console.log(`${(0, colors_1.c)('cyan', '[dev]')} listening on port: ${port}`);
26
128
  console.log(`${(0, colors_1.c)('gray', '[dev]')} starting ${(0, colors_1.c)('bold', 'tsx --watch')} and ${(0, colors_1.c)('bold', 'tsc --noEmit --watch')} (async type-checker)`);
27
129
  console.log(`${(0, colors_1.c)('gray', 'hint:')} press Ctrl+C to stop`);
28
- // Use --conditions node to ensure proper Node.js module resolution
29
- // This helps with dynamic require() calls in packages like ioredis
30
- // Only use shell on Windows where npx.cmd requires it; on Unix, direct spawn
31
- // allows proper SIGINT propagation without intermediate shell processes
32
- const useShell = process.platform === 'win32';
33
- const app = (0, child_process_1.spawn)('npx', ['-y', 'tsx', '--conditions', 'node', '--watch', entry], {
130
+ // Use --conditions node to ensure proper Node.js module resolution.
131
+ // This helps with dynamic require() calls in packages like ioredis.
132
+ // On Windows resolve npx.cmd directly previously we passed shell:true
133
+ // for the .cmd suffix, but that triggers Node DEP0190 (#381) every run.
134
+ // spawn() resolves .cmd via CreateProcessW since Node 16, so no shell is
135
+ // needed; on Unix spawn() works on 'npx' directly. SIGINT still
136
+ // propagates cleanly because no intermediate shell sits between us and
137
+ // the child process.
138
+ const npxCmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
139
+ // Issue #400 — env overlays from `frontmcp.config.env.{shared,dev}` are
140
+ // included via `resolved.effectiveEnv`. `.env`/`.env.local` already loaded
141
+ // into `process.env` above, so they win (they're closer to deployment).
142
+ const childEnv = { ...resolved.effectiveEnv, ...process.env, PORT: String(port) };
143
+ const app = (0, child_process_1.spawn)(npxCmd, ['-y', 'tsx', '--conditions', 'node', '--watch', entry], {
34
144
  stdio: 'inherit',
35
- shell: useShell,
145
+ env: childEnv,
36
146
  });
37
- const checker = (0, child_process_1.spawn)('npx', ['-y', 'tsc', '--noEmit', '--pretty', '--watch'], {
147
+ const checker = (0, child_process_1.spawn)(npxCmd, ['-y', 'tsc', '--noEmit', '--pretty', '--watch'], {
38
148
  stdio: 'inherit',
39
- shell: useShell,
149
+ env: childEnv,
40
150
  });
41
151
  const cleanup = (clearTimer = true) => {
42
152
  if (clearTimer) {
@@ -95,8 +205,13 @@ async function runDev(opts) {
95
205
  cleanup();
96
206
  process.exit(0);
97
207
  });
208
+ let appExitCode = 0;
98
209
  await new Promise((resolve, reject) => {
99
- app.on('close', () => {
210
+ app.on('close', (code) => {
211
+ // Capture the child's exit code so it can propagate to the parent
212
+ // shell. SIGINT/SIGTERM yield code=null with a signalCode — treat
213
+ // those as 0 so Ctrl+C doesn't appear as a failure.
214
+ appExitCode = typeof code === 'number' ? code : 0;
100
215
  markClosed('app');
101
216
  cleanup(false);
102
217
  resolve();
@@ -115,5 +230,10 @@ async function runDev(opts) {
115
230
  reject(err);
116
231
  });
117
232
  });
233
+ // Propagate the child's exit code so CI / shells see real failures
234
+ // instead of always-success.
235
+ if (appExitCode && appExitCode !== 0) {
236
+ process.exit(appExitCode);
237
+ }
118
238
  }
119
239
  //# sourceMappingURL=dev.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"dev.js","sourceRoot":"","sources":["../../../../src/commands/dev/dev.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\nimport { spawn, ChildProcess } from 'child_process';\nimport { ParsedArgs } from '../../core/args';\nimport { c } from '../../core/colors';\nimport { resolveEntry } from '../../shared/fs';\nimport { loadDevEnv } from '../../shared/env';\n\nfunction killQuiet(proc?: ChildProcess, signal: NodeJS.Signals = 'SIGINT') {\n try {\n if (proc && proc.exitCode === null && proc.signalCode === null) {\n proc.kill(signal);\n }\n } catch {\n // ignore\n }\n}\n\nexport async function runDev(opts: ParsedArgs): Promise<void> {\n const cwd = process.cwd();\n const entry = await resolveEntry(cwd, opts.entry);\n\n // Load .env and .env.local files before starting the server\n loadDevEnv(cwd);\n\n console.log(`${c('cyan', '[dev]')} using entry: ${path.relative(cwd, entry)}`);\n console.log(\n `${c('gray', '[dev]')} starting ${c('bold', 'tsx --watch')} and ${c(\n 'bold',\n 'tsc --noEmit --watch',\n )} (async type-checker)`,\n );\n console.log(`${c('gray', 'hint:')} press Ctrl+C to stop`);\n\n // Use --conditions node to ensure proper Node.js module resolution\n // This helps with dynamic require() calls in packages like ioredis\n // Only use shell on Windows where npx.cmd requires it; on Unix, direct spawn\n // allows proper SIGINT propagation without intermediate shell processes\n const useShell = process.platform === 'win32';\n const app = spawn('npx', ['-y', 'tsx', '--conditions', 'node', '--watch', entry], {\n stdio: 'inherit',\n shell: useShell,\n });\n const checker = spawn('npx', ['-y', 'tsc', '--noEmit', '--pretty', '--watch'], {\n stdio: 'inherit',\n shell: useShell,\n });\n\n const cleanup = (clearTimer = true) => {\n if (clearTimer) {\n clearForceKillTimer();\n }\n killQuiet(checker);\n killQuiet(app);\n };\n\n let forceKillTimer: NodeJS.Timeout | undefined;\n let appClosed = false;\n let checkerClosed = false;\n\n const clearForceKillTimer = () => {\n if (forceKillTimer) {\n clearTimeout(forceKillTimer);\n forceKillTimer = undefined;\n }\n };\n\n const markClosed = (child: 'app' | 'checker') => {\n if (child === 'app') {\n appClosed = true;\n } else {\n checkerClosed = true;\n }\n if (appClosed && checkerClosed) {\n clearForceKillTimer();\n }\n };\n\n process.once('SIGINT', () => {\n cleanup(false);\n // Force-kill after 2s if children haven't exited\n clearForceKillTimer();\n forceKillTimer = setTimeout(() => {\n killQuiet(checker, 'SIGKILL');\n killQuiet(app, 'SIGKILL');\n process.exit(0);\n }, 2000);\n forceKillTimer.unref();\n // Exit cleanly once both children have closed\n const tryExit = () => {\n if (appClosed && checkerClosed) {\n clearForceKillTimer();\n process.exit(0);\n }\n };\n app.once('close', () => {\n markClosed('app');\n tryExit();\n });\n checker.once('close', () => {\n markClosed('checker');\n tryExit();\n });\n });\n\n process.once('SIGTERM', () => {\n cleanup();\n process.exit(0);\n });\n\n await new Promise<void>((resolve, reject) => {\n app.on('close', () => {\n markClosed('app');\n cleanup(false);\n resolve();\n });\n app.on('error', (err) => {\n clearForceKillTimer();\n cleanup();\n reject(err);\n });\n checker.on('close', () => {\n markClosed('checker');\n });\n checker.on('error', (err) => {\n clearForceKillTimer();\n cleanup();\n reject(err);\n });\n });\n}\n"]}
1
+ {"version":3,"file":"dev.js","sourceRoot":"","sources":["../../../../src/commands/dev/dev.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn, type ChildProcess } from 'child_process';\nimport * as path from 'path';\n\nimport { resolveConfig } from '../../config';\nimport { type ParsedArgs } from '../../core/args';\nimport { c } from '../../core/colors';\nimport { loadDevEnv } from '../../shared/env';\nimport { resolveEntry } from '../../shared/fs';\nimport { findNextFreePort, isPortFree, lookupPortOwner } from './port';\n\nconst DEFAULT_DEV_PORT = 3000;\n\nfunction killQuiet(proc?: ChildProcess, signal: NodeJS.Signals = 'SIGINT') {\n try {\n if (proc && proc.exitCode === null && proc.signalCode === null) {\n proc.kill(signal);\n }\n } catch {\n // ignore\n }\n}\n\n/**\n * Resolve the port the dev child should bind to and report any conflict\n * clearly. Returns the chosen port — or never returns and exits the process\n * with a clear error when the port is busy and `--auto-port` was not set.\n *\n * Issue #398: previously the child crashed with a raw `EADDRINUSE` stack\n * trace; this helper turns that into a one-line message with a suggested\n * remediation and (optionally) the owning process.\n */\nexport async function resolveDevPort(opts: {\n port?: number;\n autoPort?: boolean;\n showConflict?: boolean;\n envPort?: string | undefined;\n exit?: (code: number) => never;\n log?: (msg: string) => void;\n}): Promise<number> {\n const exit = opts.exit ?? ((code: number) => process.exit(code) as never);\n const log = opts.log ?? ((msg: string) => console.error(msg));\n const explicit = opts.port ?? (opts.envPort !== undefined && opts.envPort !== '' ? Number(opts.envPort) : undefined);\n const port =\n explicit !== undefined && Number.isFinite(explicit) && (explicit as number) > 0\n ? (explicit as number)\n : DEFAULT_DEV_PORT;\n\n if (await isPortFree(port)) return port;\n\n if (opts.autoPort) {\n const alt = await findNextFreePort(port + 1);\n log(`${c('yellow', '[dev]')} port ${port} is in use; auto-picked ${alt}`);\n return alt;\n }\n\n // Build a clear, actionable error message.\n const lines = [\n `${c('red', '[dev]')} Port ${port} is already in use — refusing to start.`,\n `${c('gray', ' ')} Retry with one of:`,\n `${c('gray', ' ')} • ${c('bold', `frontmcp dev --port <other-port>`)}`,\n `${c('gray', ' ')} • ${c('bold', `frontmcp dev --auto-port`)} ${c('gray', '(pick the next free port automatically)')}`,\n `${c('gray', ' ')} • ${c('bold', `PORT=<other-port> frontmcp dev`)}`,\n ];\n if (opts.showConflict) {\n const owner = await lookupPortOwner(port);\n if (owner) {\n lines.push(`${c('gray', ' ')} Holder of ${port}:`);\n for (const row of owner.split('\\n')) lines.push(`${c('gray', ' ')} ${row}`);\n } else {\n lines.push(`${c('gray', ' ')} (could not identify the holder of port ${port})`);\n }\n } else {\n lines.push(`${c('gray', ' ')} (pass --show-conflict to print which process is holding the port)`);\n }\n for (const line of lines) log(line);\n return exit(1);\n}\n\nexport async function runDev(opts: ParsedArgs): Promise<void> {\n // Issue #399 — `--stdio` runs the first-party watch-aware stdio bridge\n // instead of the legacy `tsx --watch + tsc --noEmit --watch` pair. The\n // bridge owns process stdin/stdout (JSON-RPC frames only), holds the\n // upstream MCP session across child restarts, and replaces the\n // third-party `mcp-remote` recipe for the dev loop.\n if (opts.stdio) {\n const { runDevBridge } = await import('./bridge/index.js');\n return runDevBridge(opts);\n }\n\n const cwd = process.cwd();\n\n // Issue #400 — resolve frontmcp.config so `entry`, `transport.http.port`,\n // and `env.shared`/`env.dev` overlays apply. Precedence:\n // CLI flag > FRONTMCP_<NAME> env > frontmcp.config field > built-in default.\n const resolved = await resolveConfig({\n cwd,\n mode: 'dev',\n configPath: typeof opts.config === 'string' ? opts.config : undefined,\n });\n const cfg = resolved.config;\n\n const cliEntry = typeof opts.entry === 'string' ? opts.entry : undefined;\n const configEntry = typeof cfg?.entry === 'string' ? cfg.entry : undefined;\n const entry = await resolveEntry(cwd, cliEntry ?? configEntry);\n\n // Load .env and .env.local files (these win over config env overlays for\n // parity with existing behavior — file-based env is the deployment escape\n // hatch and shouldn't be silently overridden by committed config).\n loadDevEnv(cwd);\n\n // Resolve the port BEFORE spawning tsx so EADDRINUSE produces a clean\n // one-line error instead of a raw node:net stack trace (issue #398).\n //\n // Two caveats worth knowing about this pre-flight check:\n // 1. TOCTOU — between this probe returning and the child actually binding,\n // another process can grab the port. We accept that race: this is a\n // dev-time tool, the worst case reverts to the prior behaviour (the\n // child surfaces a raw EADDRINUSE), and the common case (port already\n // busy at startup) is the one we wanted to fix.\n // 2. The resolved port is exported as `PORT` to the child. It only takes\n // effect when the user's `@FrontMcp({ http: { port } })` reads\n // `process.env.PORT` (the SDK's `httpOptionsSchema` default does).\n // If the user's metadata HARD-CODES `http.port`, the child binds to\n // that hard-coded value and ignores PORT — the probe is then advisory\n // only. Documented in docs/frontmcp/deployment/local-dev-server.mdx.\n const cliPort = typeof opts.port === 'number' ? opts.port : opts.port ? Number(opts.port) : undefined;\n const configPort = cfg?.transport?.http?.port;\n const port = await resolveDevPort({\n port: cliPort ?? configPort,\n autoPort: !!opts.autoPort,\n showConflict: !!opts.showConflict,\n envPort: process.env['PORT'],\n });\n\n console.log(`${c('cyan', '[dev]')} using entry: ${path.relative(cwd, entry)}`);\n if (resolved.configPath || resolved.configDir) {\n console.log(`${c('gray', '[dev]')} config: ${resolved.configPath ?? resolved.configDir}`);\n }\n console.log(`${c('cyan', '[dev]')} listening on port: ${port}`);\n console.log(\n `${c('gray', '[dev]')} starting ${c('bold', 'tsx --watch')} and ${c(\n 'bold',\n 'tsc --noEmit --watch',\n )} (async type-checker)`,\n );\n console.log(`${c('gray', 'hint:')} press Ctrl+C to stop`);\n\n // Use --conditions node to ensure proper Node.js module resolution.\n // This helps with dynamic require() calls in packages like ioredis.\n // On Windows resolve npx.cmd directly — previously we passed shell:true\n // for the .cmd suffix, but that triggers Node DEP0190 (#381) every run.\n // spawn() resolves .cmd via CreateProcessW since Node 16, so no shell is\n // needed; on Unix spawn() works on 'npx' directly. SIGINT still\n // propagates cleanly because no intermediate shell sits between us and\n // the child process.\n const npxCmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';\n // Issue #400 — env overlays from `frontmcp.config.env.{shared,dev}` are\n // included via `resolved.effectiveEnv`. `.env`/`.env.local` already loaded\n // into `process.env` above, so they win (they're closer to deployment).\n const childEnv = { ...resolved.effectiveEnv, ...process.env, PORT: String(port) };\n const app = spawn(npxCmd, ['-y', 'tsx', '--conditions', 'node', '--watch', entry], {\n stdio: 'inherit',\n env: childEnv,\n });\n const checker = spawn(npxCmd, ['-y', 'tsc', '--noEmit', '--pretty', '--watch'], {\n stdio: 'inherit',\n env: childEnv,\n });\n\n const cleanup = (clearTimer = true) => {\n if (clearTimer) {\n clearForceKillTimer();\n }\n killQuiet(checker);\n killQuiet(app);\n };\n\n let forceKillTimer: NodeJS.Timeout | undefined;\n let appClosed = false;\n let checkerClosed = false;\n\n const clearForceKillTimer = () => {\n if (forceKillTimer) {\n clearTimeout(forceKillTimer);\n forceKillTimer = undefined;\n }\n };\n\n const markClosed = (child: 'app' | 'checker') => {\n if (child === 'app') {\n appClosed = true;\n } else {\n checkerClosed = true;\n }\n if (appClosed && checkerClosed) {\n clearForceKillTimer();\n }\n };\n\n process.once('SIGINT', () => {\n cleanup(false);\n // Force-kill after 2s if children haven't exited\n clearForceKillTimer();\n forceKillTimer = setTimeout(() => {\n killQuiet(checker, 'SIGKILL');\n killQuiet(app, 'SIGKILL');\n process.exit(0);\n }, 2000);\n forceKillTimer.unref();\n // Exit cleanly once both children have closed\n const tryExit = () => {\n if (appClosed && checkerClosed) {\n clearForceKillTimer();\n process.exit(0);\n }\n };\n app.once('close', () => {\n markClosed('app');\n tryExit();\n });\n checker.once('close', () => {\n markClosed('checker');\n tryExit();\n });\n });\n\n process.once('SIGTERM', () => {\n cleanup();\n process.exit(0);\n });\n\n let appExitCode: number | null = 0;\n await new Promise<void>((resolve, reject) => {\n app.on('close', (code) => {\n // Capture the child's exit code so it can propagate to the parent\n // shell. SIGINT/SIGTERM yield code=null with a signalCode — treat\n // those as 0 so Ctrl+C doesn't appear as a failure.\n appExitCode = typeof code === 'number' ? code : 0;\n markClosed('app');\n cleanup(false);\n resolve();\n });\n app.on('error', (err) => {\n clearForceKillTimer();\n cleanup();\n reject(err);\n });\n checker.on('close', () => {\n markClosed('checker');\n });\n checker.on('error', (err) => {\n clearForceKillTimer();\n cleanup();\n reject(err);\n });\n });\n\n // Propagate the child's exit code so CI / shells see real failures\n // instead of always-success.\n if (appExitCode && appExitCode !== 0) {\n process.exit(appExitCode);\n }\n}\n"]}
@@ -1 +1,13 @@
1
- export declare function runInspector(): Promise<void>;
1
+ import { type FrontMcpConfigParsed } from '../../config';
2
+ import { type ParsedArgs } from '../../core/args';
3
+ export declare function buildInspectorArgs(config: FrontMcpConfigParsed | undefined): string[];
4
+ /**
5
+ * Launch MCP Inspector against the configured transport (issue #400).
6
+ *
7
+ * Reads `transport.default` and `transport.http.port` from the resolved
8
+ * `frontmcp.config` to build the inspector args automatically. When the
9
+ * config selects HTTP, the inspector is pointed at the configured URL so
10
+ * users don't have to type it. When no config is found we fall back to the
11
+ * stock launch (inspector with no transport target — interactive picker).
12
+ */
13
+ export declare function runInspector(opts?: ParsedArgs): Promise<void>;
@@ -1,10 +1,84 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildInspectorArgs = buildInspectorArgs;
3
4
  exports.runInspector = runInspector;
4
- const colors_1 = require("../../core/colors");
5
5
  const utils_1 = require("@frontmcp/utils");
6
- async function runInspector() {
6
+ const config_1 = require("../../config");
7
+ const colors_1 = require("../../core/colors");
8
+ /**
9
+ * Build the argv passed to `npx` to launch the modern MCP Inspector
10
+ * (`@modelcontextprotocol/inspector`).
11
+ *
12
+ * The Inspector UI mode CLI surface (verified against
13
+ * github.com/modelcontextprotocol/inspector/blob/main/cli/src/cli.ts) is:
14
+ *
15
+ * inspector [-e KEY=VALUE]... [--transport stdio|sse|http]
16
+ * [--server-url <url>] [--header "H: V"] [--] <command> [args...]
17
+ *
18
+ * - `--transport http` is shorthand and is remapped to `streamable-http`
19
+ * internally.
20
+ * - For stdio, the server is supplied as **positional** `<command> [args...]`
21
+ * after `--` (separator so leading-dash args aren't parsed as Inspector
22
+ * flags). There is no `--server-command` / `--server-args` pair — those
23
+ * are URL query params for the browser UI, not CLI flags.
24
+ *
25
+ * Exported for unit-testing without spawning npx.
26
+ */
27
+ /**
28
+ * Ensure a mount path starts with exactly one leading slash and has no
29
+ * accidental `//` segments — a user-configured `transport.http.path` like
30
+ * `'mcp'` or `'//api/'` would otherwise produce malformed URLs such as
31
+ * `http://host:port//api/` that the Inspector can't connect to.
32
+ */
33
+ function normalizeMountPath(raw) {
34
+ const withLead = raw.startsWith('/') ? raw : `/${raw}`;
35
+ return withLead.replace(/\/{2,}/g, '/');
36
+ }
37
+ function buildInspectorArgs(config) {
38
+ const args = ['-y', '@modelcontextprotocol/inspector'];
39
+ const transport = config?.transport;
40
+ if (transport?.default === 'http' && transport.http?.port) {
41
+ const host = transport.http.host ?? '127.0.0.1';
42
+ const mountPath = normalizeMountPath(transport.http.path ?? '/mcp');
43
+ args.push('--transport', 'http', '--server-url', `http://${host}:${transport.http.port}${mountPath}`);
44
+ }
45
+ else if (transport?.default === 'sse' && transport.http?.port) {
46
+ const host = transport.http.host ?? '127.0.0.1';
47
+ // SSE has no configurable path on transport.http (only `/sse` by
48
+ // convention) but normalise the literal anyway so any future schema
49
+ // change to surface a custom SSE path can't slip through with `//`.
50
+ const ssePath = normalizeMountPath('/sse');
51
+ args.push('--transport', 'sse', '--server-url', `http://${host}:${transport.http.port}${ssePath}`);
52
+ }
53
+ else if (transport?.default === 'stdio' && transport.stdio?.command) {
54
+ args.push('--transport', 'stdio', '--', transport.stdio.command, ...(transport.stdio.args ?? []));
55
+ }
56
+ return args;
57
+ }
58
+ /**
59
+ * Launch MCP Inspector against the configured transport (issue #400).
60
+ *
61
+ * Reads `transport.default` and `transport.http.port` from the resolved
62
+ * `frontmcp.config` to build the inspector args automatically. When the
63
+ * config selects HTTP, the inspector is pointed at the configured URL so
64
+ * users don't have to type it. When no config is found we fall back to the
65
+ * stock launch (inspector with no transport target — interactive picker).
66
+ */
67
+ async function runInspector(opts = { _: [] }) {
68
+ const resolved = await (0, config_1.resolveConfig)({
69
+ cwd: process.cwd(),
70
+ mode: 'inspector',
71
+ configPath: typeof opts.config === 'string' ? opts.config : undefined,
72
+ });
73
+ const args = buildInspectorArgs(resolved.config);
7
74
  console.log(`${(0, colors_1.c)('cyan', '[inspector]')} launching MCP Inspector...`);
8
- await (0, utils_1.runCmd)('npx', ['-y', '@modelcontextprotocol/inspector']);
75
+ if (resolved.configPath || resolved.configDir) {
76
+ console.log(`${(0, colors_1.c)('gray', '[inspector]')} config: ${resolved.configPath ?? resolved.configDir}`);
77
+ }
78
+ // Issue #400 — forward the resolved env overlay (process.env ⊕ config.env.shared
79
+ // ⊕ config.env.dev) to the Inspector child so any env-gated server config
80
+ // (API keys, feature flags) the user keeps in `frontmcp.config.env.dev` is
81
+ // visible to the MCP server the Inspector spawns under it.
82
+ await (0, utils_1.runCmd)('npx', args, { env: resolved.effectiveEnv, cwd: process.cwd() });
9
83
  }
10
84
  //# sourceMappingURL=inspector.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"inspector.js","sourceRoot":"","sources":["../../../../src/commands/dev/inspector.ts"],"names":[],"mappings":";;AAGA,oCAGC;AAND,8CAAsC;AACtC,2CAAyC;AAElC,KAAK,UAAU,YAAY;IAChC,OAAO,CAAC,GAAG,CAAC,GAAG,IAAA,UAAC,EAAC,MAAM,EAAE,aAAa,CAAC,6BAA6B,CAAC,CAAC;IACtE,MAAM,IAAA,cAAM,EAAC,KAAK,EAAE,CAAC,IAAI,EAAE,iCAAiC,CAAC,CAAC,CAAC;AACjE,CAAC","sourcesContent":["import { c } from '../../core/colors';\nimport { runCmd } from '@frontmcp/utils';\n\nexport async function runInspector(): Promise<void> {\n console.log(`${c('cyan', '[inspector]')} launching MCP Inspector...`);\n await runCmd('npx', ['-y', '@modelcontextprotocol/inspector']);\n}\n"]}
1
+ {"version":3,"file":"inspector.js","sourceRoot":"","sources":["../../../../src/commands/dev/inspector.ts"],"names":[],"mappings":"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","sourcesContent":["import { runCmd } from '@frontmcp/utils';\n\nimport { resolveConfig, type FrontMcpConfigParsed } from '../../config';\nimport { type ParsedArgs } from '../../core/args';\nimport { c } from '../../core/colors';\n\n/**\n * Build the argv passed to `npx` to launch the modern MCP Inspector\n * (`@modelcontextprotocol/inspector`).\n *\n * The Inspector UI mode CLI surface (verified against\n * github.com/modelcontextprotocol/inspector/blob/main/cli/src/cli.ts) is:\n *\n * inspector [-e KEY=VALUE]... [--transport stdio|sse|http]\n * [--server-url <url>] [--header \"H: V\"] [--] <command> [args...]\n *\n * - `--transport http` is shorthand and is remapped to `streamable-http`\n * internally.\n * - For stdio, the server is supplied as **positional** `<command> [args...]`\n * after `--` (separator so leading-dash args aren't parsed as Inspector\n * flags). There is no `--server-command` / `--server-args` pair — those\n * are URL query params for the browser UI, not CLI flags.\n *\n * Exported for unit-testing without spawning npx.\n */\n/**\n * Ensure a mount path starts with exactly one leading slash and has no\n * accidental `//` segments — a user-configured `transport.http.path` like\n * `'mcp'` or `'//api/'` would otherwise produce malformed URLs such as\n * `http://host:port//api/` that the Inspector can't connect to.\n */\nfunction normalizeMountPath(raw: string): string {\n const withLead = raw.startsWith('/') ? raw : `/${raw}`;\n return withLead.replace(/\\/{2,}/g, '/');\n}\n\nexport function buildInspectorArgs(config: FrontMcpConfigParsed | undefined): string[] {\n const args: string[] = ['-y', '@modelcontextprotocol/inspector'];\n const transport = config?.transport;\n if (transport?.default === 'http' && transport.http?.port) {\n const host = transport.http.host ?? '127.0.0.1';\n const mountPath = normalizeMountPath(transport.http.path ?? '/mcp');\n args.push('--transport', 'http', '--server-url', `http://${host}:${transport.http.port}${mountPath}`);\n } else if (transport?.default === 'sse' && transport.http?.port) {\n const host = transport.http.host ?? '127.0.0.1';\n // SSE has no configurable path on transport.http (only `/sse` by\n // convention) but normalise the literal anyway so any future schema\n // change to surface a custom SSE path can't slip through with `//`.\n const ssePath = normalizeMountPath('/sse');\n args.push('--transport', 'sse', '--server-url', `http://${host}:${transport.http.port}${ssePath}`);\n } else if (transport?.default === 'stdio' && transport.stdio?.command) {\n args.push('--transport', 'stdio', '--', transport.stdio.command, ...(transport.stdio.args ?? []));\n }\n return args;\n}\n\n/**\n * Launch MCP Inspector against the configured transport (issue #400).\n *\n * Reads `transport.default` and `transport.http.port` from the resolved\n * `frontmcp.config` to build the inspector args automatically. When the\n * config selects HTTP, the inspector is pointed at the configured URL so\n * users don't have to type it. When no config is found we fall back to the\n * stock launch (inspector with no transport target — interactive picker).\n */\nexport async function runInspector(opts: ParsedArgs = { _: [] } as unknown as ParsedArgs): Promise<void> {\n const resolved = await resolveConfig({\n cwd: process.cwd(),\n mode: 'inspector',\n configPath: typeof opts.config === 'string' ? opts.config : undefined,\n });\n\n const args = buildInspectorArgs(resolved.config);\n\n console.log(`${c('cyan', '[inspector]')} launching MCP Inspector...`);\n if (resolved.configPath || resolved.configDir) {\n console.log(`${c('gray', '[inspector]')} config: ${resolved.configPath ?? resolved.configDir}`);\n }\n // Issue #400 — forward the resolved env overlay (process.env ⊕ config.env.shared\n // ⊕ config.env.dev) to the Inspector child so any env-gated server config\n // (API keys, feature flags) the user keeps in `frontmcp.config.env.dev` is\n // visible to the MCP server the Inspector spawns under it.\n await runCmd('npx', args, { env: resolved.effectiveEnv, cwd: process.cwd() });\n}\n"]}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Probe a TCP port by binding a throwaway server.
3
+ *
4
+ * Resolves `true` when the port is free (bind succeeds; the server is closed
5
+ * immediately) and `false` when bind fails with `EADDRINUSE`. Other errors
6
+ * (permission denied on privileged ports, invalid host, …) are rethrown so
7
+ * callers see the real cause instead of a misleading "in use" signal.
8
+ */
9
+ export declare function isPortFree(port: number, host?: string): Promise<boolean>;
10
+ /**
11
+ * Walk forward from `start` until a free TCP port is found. Caps the probe
12
+ * count to avoid pathological scans when an attacker / mistake holds a huge
13
+ * contiguous range.
14
+ *
15
+ * @throws Error when no free port is found within the cap.
16
+ */
17
+ export declare function findNextFreePort(start: number, host?: string, maxProbes?: number): Promise<number>;
18
+ /**
19
+ * Identify which process is holding a port. Opt-in via `--show-conflict`
20
+ * because lsof can be slow and is unavailable on Windows. Returns
21
+ * `undefined` when lsof is not on PATH or the lookup produced no rows.
22
+ */
23
+ export declare function lookupPortOwner(port: number): Promise<string | undefined>;
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ // libs/cli/src/commands/dev/port.ts
3
+ //
4
+ // Port-probing helpers for `frontmcp dev`. Extracted so they can be unit-tested
5
+ // against real ephemeral ports without spawning the dev pipeline (issue #398).
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.isPortFree = isPortFree;
8
+ exports.findNextFreePort = findNextFreePort;
9
+ exports.lookupPortOwner = lookupPortOwner;
10
+ const tslib_1 = require("tslib");
11
+ const node_child_process_1 = require("node:child_process");
12
+ const net = tslib_1.__importStar(require("node:net"));
13
+ const node_util_1 = require("node:util");
14
+ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
15
+ const DEFAULT_HOST = '127.0.0.1';
16
+ const MAX_AUTO_PORT_PROBES = 50;
17
+ /**
18
+ * Probe a TCP port by binding a throwaway server.
19
+ *
20
+ * Resolves `true` when the port is free (bind succeeds; the server is closed
21
+ * immediately) and `false` when bind fails with `EADDRINUSE`. Other errors
22
+ * (permission denied on privileged ports, invalid host, …) are rethrown so
23
+ * callers see the real cause instead of a misleading "in use" signal.
24
+ */
25
+ async function isPortFree(port, host = DEFAULT_HOST) {
26
+ return new Promise((resolve, reject) => {
27
+ const server = net.createServer();
28
+ const onError = (err) => {
29
+ server.removeAllListeners();
30
+ if (err.code === 'EADDRINUSE') {
31
+ resolve(false);
32
+ }
33
+ else {
34
+ reject(err);
35
+ }
36
+ };
37
+ server.once('error', onError);
38
+ server.once('listening', () => {
39
+ server.removeAllListeners();
40
+ server.close(() => resolve(true));
41
+ });
42
+ // exclusive: true so we don't share via SO_REUSEPORT.
43
+ server.listen({ port, host, exclusive: true });
44
+ });
45
+ }
46
+ /**
47
+ * Walk forward from `start` until a free TCP port is found. Caps the probe
48
+ * count to avoid pathological scans when an attacker / mistake holds a huge
49
+ * contiguous range.
50
+ *
51
+ * @throws Error when no free port is found within the cap.
52
+ */
53
+ async function findNextFreePort(start, host = DEFAULT_HOST, maxProbes = MAX_AUTO_PORT_PROBES) {
54
+ for (let i = 0; i < maxProbes; i++) {
55
+ const port = start + i;
56
+ if (port > 65535)
57
+ break;
58
+ if (await isPortFree(port, host))
59
+ return port;
60
+ }
61
+ throw new Error(`No free TCP port found in range ${start}..${start + maxProbes - 1}.`);
62
+ }
63
+ /**
64
+ * Identify which process is holding a port. Opt-in via `--show-conflict`
65
+ * because lsof can be slow and is unavailable on Windows. Returns
66
+ * `undefined` when lsof is not on PATH or the lookup produced no rows.
67
+ */
68
+ async function lookupPortOwner(port) {
69
+ if (process.platform === 'win32') {
70
+ // `lsof` isn't a thing here; netstat is the equivalent but its output
71
+ // shape varies across Windows versions, so leave this out for now.
72
+ return undefined;
73
+ }
74
+ try {
75
+ const { stdout } = await execFileAsync('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN'], {
76
+ timeout: 1500,
77
+ });
78
+ const lines = stdout.split('\n').filter((l) => l.trim().length > 0);
79
+ if (lines.length < 2)
80
+ return undefined;
81
+ return lines.slice(1).join('\n').trim();
82
+ }
83
+ catch {
84
+ return undefined;
85
+ }
86
+ }
87
+ //# sourceMappingURL=port.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"port.js","sourceRoot":"","sources":["../../../../src/commands/dev/port.ts"],"names":[],"mappings":"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","sourcesContent":["// libs/cli/src/commands/dev/port.ts\n//\n// Port-probing helpers for `frontmcp dev`. Extracted so they can be unit-tested\n// against real ephemeral ports without spawning the dev pipeline (issue #398).\n\nimport { execFile } from 'node:child_process';\nimport * as net from 'node:net';\nimport { promisify } from 'node:util';\n\nconst execFileAsync = promisify(execFile);\n\nconst DEFAULT_HOST = '127.0.0.1';\nconst MAX_AUTO_PORT_PROBES = 50;\n\n/**\n * Probe a TCP port by binding a throwaway server.\n *\n * Resolves `true` when the port is free (bind succeeds; the server is closed\n * immediately) and `false` when bind fails with `EADDRINUSE`. Other errors\n * (permission denied on privileged ports, invalid host, …) are rethrown so\n * callers see the real cause instead of a misleading \"in use\" signal.\n */\nexport async function isPortFree(port: number, host: string = DEFAULT_HOST): Promise<boolean> {\n return new Promise((resolve, reject) => {\n const server = net.createServer();\n const onError = (err: NodeJS.ErrnoException) => {\n server.removeAllListeners();\n if (err.code === 'EADDRINUSE') {\n resolve(false);\n } else {\n reject(err);\n }\n };\n server.once('error', onError);\n server.once('listening', () => {\n server.removeAllListeners();\n server.close(() => resolve(true));\n });\n // exclusive: true so we don't share via SO_REUSEPORT.\n server.listen({ port, host, exclusive: true });\n });\n}\n\n/**\n * Walk forward from `start` until a free TCP port is found. Caps the probe\n * count to avoid pathological scans when an attacker / mistake holds a huge\n * contiguous range.\n *\n * @throws Error when no free port is found within the cap.\n */\nexport async function findNextFreePort(\n start: number,\n host: string = DEFAULT_HOST,\n maxProbes: number = MAX_AUTO_PORT_PROBES,\n): Promise<number> {\n for (let i = 0; i < maxProbes; i++) {\n const port = start + i;\n if (port > 65535) break;\n\n if (await isPortFree(port, host)) return port;\n }\n throw new Error(`No free TCP port found in range ${start}..${start + maxProbes - 1}.`);\n}\n\n/**\n * Identify which process is holding a port. Opt-in via `--show-conflict`\n * because lsof can be slow and is unavailable on Windows. Returns\n * `undefined` when lsof is not on PATH or the lookup produced no rows.\n */\nexport async function lookupPortOwner(port: number): Promise<string | undefined> {\n if (process.platform === 'win32') {\n // `lsof` isn't a thing here; netstat is the equivalent but its output\n // shape varies across Windows versions, so leave this out for now.\n return undefined;\n }\n try {\n const { stdout } = await execFileAsync('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN'], {\n timeout: 1500,\n });\n const lines = stdout.split('\\n').filter((l) => l.trim().length > 0);\n if (lines.length < 2) return undefined;\n return lines.slice(1).join('\\n').trim();\n } catch {\n return undefined;\n }\n}\n"]}
@@ -1,2 +1,2 @@
1
- import { Command } from 'commander';
1
+ import { type Command } from 'commander';
2
2
  export declare function registerDevCommands(program: Command): void;
@@ -7,8 +7,25 @@ function registerDevCommands(program) {
7
7
  .command('dev')
8
8
  .description('Start in development mode (tsx --watch + async type-check)')
9
9
  .option('-e, --entry <path>', 'Entry file path')
10
- .action(async (options) => {
10
+ .option('-p, --port <port>', 'TCP port to listen on (sets PORT env for the child)', (v) => parseInt(v, 10))
11
+ .option('--auto-port', 'If the chosen port is busy, auto-pick the next free port')
12
+ .option('--show-conflict', 'On EADDRINUSE, print the process holding the port (uses lsof on POSIX)')
13
+ // Issue #399 — first-party watch-aware stdio bridge. Replaces the
14
+ // `npx mcp-remote` recipe for the dev loop; the bridge holds the
15
+ // stdio connection across user-code restarts so MCP clients (Claude
16
+ // Code, etc.) don't sit on `Calling…` after every save.
17
+ .option('--stdio', 'Run frontmcp dev as a stdio bridge for an MCP client')
18
+ .option('--serve', 'Use stdio-over-pipe to the child (default: HTTP/SSE loopback)')
19
+ .option('--log-file <path>', 'Bridge log file path', './.frontmcp/dev.log')
20
+ .option('--buffer-size <n>', 'Max RPCs buffered during reload', (v) => parseInt(v, 10))
21
+ .option('--reload-deadline-ms <ms>', 'Time to wait for a reload to complete', (v) => parseInt(v, 10))
22
+ .action(async (options, cmd) => {
11
23
  const { runDev } = await import('./dev.js');
24
+ // Issue #400 — forward the top-level --config flag into the command's
25
+ // ParsedArgs so `runDev`'s resolveConfig() call picks it up.
26
+ const topOpts = cmd.parent?.opts?.() ?? {};
27
+ if (typeof topOpts['config'] === 'string')
28
+ options.config = topOpts['config'];
12
29
  await runDev((0, bridge_1.toParsedArgs)('dev', [], options));
13
30
  });
14
31
  program
@@ -20,8 +37,11 @@ function registerDevCommands(program) {
20
37
  .option('-v, --verbose', 'Show verbose test output')
21
38
  .option('-t, --timeout <ms>', 'Set test timeout (default: 60000ms)', parseInt)
22
39
  .option('-c, --coverage', 'Collect test coverage')
23
- .action(async (patterns, options) => {
40
+ .action(async (patterns, options, cmd) => {
24
41
  const { runTest } = await import('./test.js');
42
+ const topOpts = cmd.parent?.opts?.() ?? {};
43
+ if (typeof topOpts['config'] === 'string')
44
+ options.config = topOpts['config'];
25
45
  await runTest((0, bridge_1.toParsedArgs)('test', patterns, options));
26
46
  });
27
47
  program
@@ -41,9 +61,13 @@ function registerDevCommands(program) {
41
61
  program
42
62
  .command('inspector')
43
63
  .description('Launch MCP Inspector (npx @modelcontextprotocol/inspector)')
44
- .action(async () => {
64
+ .action(async (_args, cmd) => {
45
65
  const { runInspector } = await import('./inspector.js');
46
- await runInspector();
66
+ const opts = cmd.parent?.opts?.() ?? {};
67
+ await runInspector({
68
+ _: [],
69
+ config: typeof opts['config'] === 'string' ? opts['config'] : undefined,
70
+ });
47
71
  });
48
72
  }
49
73
  //# sourceMappingURL=register.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"register.js","sourceRoot":"","sources":["../../../../src/commands/dev/register.ts"],"names":[],"mappings":";;AAGA,kDA+CC;AAjDD,8CAAiD;AAEjD,SAAgB,mBAAmB,CAAC,OAAgB;IAClD,OAAO;SACJ,OAAO,CAAC,KAAK,CAAC;SACd,WAAW,CAAC,4DAA4D,CAAC;SACzE,MAAM,CAAC,oBAAoB,EAAE,iBAAiB,CAAC;SAC/C,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;QACxB,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,CAAC;QAC5C,MAAM,MAAM,CAAC,IAAA,qBAAY,EAAC,KAAK,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;IAEL,OAAO;SACJ,OAAO,CAAC,MAAM,CAAC;SACf,WAAW,CAAC,qDAAqD,CAAC;SAClE,QAAQ,CAAC,eAAe,EAAE,oBAAoB,CAAC;SAC/C,MAAM,CAAC,iBAAiB,EAAE,8CAA8C,CAAC;SACzE,MAAM,CAAC,aAAa,EAAE,yBAAyB,CAAC;SAChD,MAAM,CAAC,eAAe,EAAE,0BAA0B,CAAC;SACnD,MAAM,CAAC,oBAAoB,EAAE,qCAAqC,EAAE,QAAQ,CAAC;SAC7E,MAAM,CAAC,gBAAgB,EAAE,uBAAuB,CAAC;SACjD,MAAM,CAAC,KAAK,EAAE,QAAkB,EAAE,OAAO,EAAE,EAAE;QAC5C,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,CAAC;QAC9C,MAAM,OAAO,CAAC,IAAA,qBAAY,EAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;IAEL,OAAO;SACJ,OAAO,CAAC,MAAM,CAAC;SACf,WAAW,CAAC,qDAAqD,CAAC;SAClE,MAAM,CAAC,KAAK,IAAI,EAAE;QACjB,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,wBAAwB,CAAC,CAAC;QAC3D,MAAM,OAAO,EAAE,CAAC;IAClB,CAAC,CAAC,CAAC;IAEL,OAAO;SACJ,OAAO,CAAC,QAAQ,CAAC;SACjB,WAAW,CAAC,mDAAmD,CAAC;SAChE,MAAM,CAAC,KAAK,IAAI,EAAE;QACjB,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC;QAClD,MAAM,SAAS,EAAE,CAAC;IACpB,CAAC,CAAC,CAAC;IAEL,OAAO;SACJ,OAAO,CAAC,WAAW,CAAC;SACpB,WAAW,CAAC,4DAA4D,CAAC;SACzE,MAAM,CAAC,KAAK,IAAI,EAAE;QACjB,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,MAAM,CAAC,gBAAgB,CAAC,CAAC;QACxD,MAAM,YAAY,EAAE,CAAC;IACvB,CAAC,CAAC,CAAC;AACP,CAAC","sourcesContent":["import { Command } from 'commander';\nimport { toParsedArgs } from '../../core/bridge';\n\nexport function registerDevCommands(program: Command): void {\n program\n .command('dev')\n .description('Start in development mode (tsx --watch + async type-check)')\n .option('-e, --entry <path>', 'Entry file path')\n .action(async (options) => {\n const { runDev } = await import('./dev.js');\n await runDev(toParsedArgs('dev', [], options));\n });\n\n program\n .command('test')\n .description('Run E2E tests with auto-injected Jest configuration')\n .argument('[patterns...]', 'Test file patterns')\n .option('-i, --runInBand', 'Run tests sequentially (recommended for E2E)')\n .option('-w, --watch', 'Run tests in watch mode')\n .option('-v, --verbose', 'Show verbose test output')\n .option('-t, --timeout <ms>', 'Set test timeout (default: 60000ms)', parseInt)\n .option('-c, --coverage', 'Collect test coverage')\n .action(async (patterns: string[], options) => {\n const { runTest } = await import('./test.js');\n await runTest(toParsedArgs('test', patterns, options));\n });\n\n program\n .command('init')\n .description('Create or fix a tsconfig.json suitable for FrontMCP')\n .action(async () => {\n const { runInit } = await import('../../core/tsconfig.js');\n await runInit();\n });\n\n program\n .command('doctor')\n .description('Check Node/npm versions and tsconfig requirements')\n .action(async () => {\n const { runDoctor } = await import('./doctor.js');\n await runDoctor();\n });\n\n program\n .command('inspector')\n .description('Launch MCP Inspector (npx @modelcontextprotocol/inspector)')\n .action(async () => {\n const { runInspector } = await import('./inspector.js');\n await runInspector();\n });\n}\n"]}
1
+ {"version":3,"file":"register.js","sourceRoot":"","sources":["../../../../src/commands/dev/register.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Command } from 'commander';\n\nimport { toParsedArgs } from '../../core/bridge';\n\nexport function registerDevCommands(program: Command): void {\n program\n .command('dev')\n .description('Start in development mode (tsx --watch + async type-check)')\n .option('-e, --entry <path>', 'Entry file path')\n .option('-p, --port <port>', 'TCP port to listen on (sets PORT env for the child)', (v) => parseInt(v, 10))\n .option('--auto-port', 'If the chosen port is busy, auto-pick the next free port')\n .option('--show-conflict', 'On EADDRINUSE, print the process holding the port (uses lsof on POSIX)')\n // Issue #399 — first-party watch-aware stdio bridge. Replaces the\n // `npx mcp-remote` recipe for the dev loop; the bridge holds the\n // stdio connection across user-code restarts so MCP clients (Claude\n // Code, etc.) don't sit on `Calling…` after every save.\n .option('--stdio', 'Run frontmcp dev as a stdio bridge for an MCP client')\n .option('--serve', 'Use stdio-over-pipe to the child (default: HTTP/SSE loopback)')\n .option('--log-file <path>', 'Bridge log file path', './.frontmcp/dev.log')\n .option('--buffer-size <n>', 'Max RPCs buffered during reload', (v) => parseInt(v, 10))\n .option('--reload-deadline-ms <ms>', 'Time to wait for a reload to complete', (v) => parseInt(v, 10))\n .action(async (options, cmd: { parent?: { opts?: () => Record<string, unknown> } }) => {\n const { runDev } = await import('./dev.js');\n // Issue #400 — forward the top-level --config flag into the command's\n // ParsedArgs so `runDev`'s resolveConfig() call picks it up.\n const topOpts = cmd.parent?.opts?.() ?? {};\n if (typeof topOpts['config'] === 'string') options.config = topOpts['config'];\n await runDev(toParsedArgs('dev', [], options));\n });\n\n program\n .command('test')\n .description('Run E2E tests with auto-injected Jest configuration')\n .argument('[patterns...]', 'Test file patterns')\n .option('-i, --runInBand', 'Run tests sequentially (recommended for E2E)')\n .option('-w, --watch', 'Run tests in watch mode')\n .option('-v, --verbose', 'Show verbose test output')\n .option('-t, --timeout <ms>', 'Set test timeout (default: 60000ms)', parseInt)\n .option('-c, --coverage', 'Collect test coverage')\n .action(async (patterns: string[], options, cmd: { parent?: { opts?: () => Record<string, unknown> } }) => {\n const { runTest } = await import('./test.js');\n const topOpts = cmd.parent?.opts?.() ?? {};\n if (typeof topOpts['config'] === 'string') options.config = topOpts['config'];\n await runTest(toParsedArgs('test', patterns, options));\n });\n\n program\n .command('init')\n .description('Create or fix a tsconfig.json suitable for FrontMCP')\n .action(async () => {\n const { runInit } = await import('../../core/tsconfig.js');\n await runInit();\n });\n\n program\n .command('doctor')\n .description('Check Node/npm versions and tsconfig requirements')\n .action(async () => {\n const { runDoctor } = await import('./doctor.js');\n await runDoctor();\n });\n\n program\n .command('inspector')\n .description('Launch MCP Inspector (npx @modelcontextprotocol/inspector)')\n .action(async (_args, cmd: { parent?: { opts?: () => Record<string, unknown> } }) => {\n const { runInspector } = await import('./inspector.js');\n const opts = cmd.parent?.opts?.() ?? {};\n await runInspector({\n _: [],\n config: typeof opts['config'] === 'string' ? (opts['config'] as string) : undefined,\n } as never);\n });\n}\n"]}
@@ -1,4 +1,29 @@
1
- import { ParsedArgs } from '../../core/args';
1
+ import { type ParsedArgs } from '../../core/args';
2
+ export declare function findUserJestConfig(cwd: string): Promise<string | undefined>;
3
+ /**
4
+ * Build the args passed to `npx jest`. Extracted from `runTest` so the args
5
+ * matrix can be unit-tested without spawning a subprocess.
6
+ *
7
+ * @internal
8
+ */
9
+ export declare function buildJestArgs(configPath: string, opts: ParsedArgs, positionalPatterns?: string[]): string[];
10
+ /**
11
+ * Generate Jest configuration programmatically.
12
+ *
13
+ * Issue #402: the original config (a) only ran `e2e/**` and `**\/*.e2e.ts`,
14
+ * missing the `*.spec.ts(x)` colocated unit tests mandated by CLAUDE.md, and
15
+ * (b) only transformed `.ts`/`.js` with a `typescript` parser, so any `.tsx`
16
+ * file failed both the regex AND SWC's parser. This generator now:
17
+ * - matches both colocated unit specs and e2e specs (`.ts` + `.tsx`),
18
+ * - transforms `.tsx`/`.jsx` files with `tsx: true` and the automatic JSX
19
+ * runtime so React components are usable in tests,
20
+ * - exposes the helper for unit testing.
21
+ */
22
+ export declare function generateJestConfig(cwd: string, opts: ParsedArgs, testDefaults?: {
23
+ timeoutMs?: number;
24
+ testMatch?: string[];
25
+ coverage?: boolean;
26
+ }): object;
2
27
  /**
3
28
  * Run E2E tests using Jest with auto-injected configuration.
4
29
  *