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.
- package/package.json +4 -4
- package/src/commands/build/exec/bin-meta.d.ts +49 -0
- package/src/commands/build/exec/bin-meta.js +68 -0
- package/src/commands/build/exec/bin-meta.js.map +1 -0
- package/src/commands/build/exec/cli-runtime/generate-cli-entry.js +195 -3
- package/src/commands/build/exec/cli-runtime/generate-cli-entry.js.map +1 -1
- package/src/commands/build/exec/cli-runtime/plugin-emitter.d.ts +160 -0
- package/src/commands/build/exec/cli-runtime/plugin-emitter.js +512 -0
- package/src/commands/build/exec/cli-runtime/plugin-emitter.js.map +1 -0
- package/src/commands/build/exec/cli-runtime/schema-extractor.d.ts +13 -1
- package/src/commands/build/exec/cli-runtime/schema-extractor.js +29 -3
- package/src/commands/build/exec/cli-runtime/schema-extractor.js.map +1 -1
- package/src/commands/build/exec/cli-runtime/skill-md-compose.d.ts +25 -0
- package/src/commands/build/exec/cli-runtime/skill-md-compose.js +63 -0
- package/src/commands/build/exec/cli-runtime/skill-md-compose.js.map +1 -0
- package/src/commands/build/exec/index.js +26 -0
- package/src/commands/build/exec/index.js.map +1 -1
- package/src/commands/dev/bridge/child-supervisor.d.ts +48 -0
- package/src/commands/dev/bridge/child-supervisor.js +228 -0
- package/src/commands/dev/bridge/child-supervisor.js.map +1 -0
- package/src/commands/dev/bridge/errors.d.ts +23 -0
- package/src/commands/dev/bridge/errors.js +34 -0
- package/src/commands/dev/bridge/errors.js.map +1 -0
- package/src/commands/dev/bridge/index.d.ts +30 -0
- package/src/commands/dev/bridge/index.js +220 -0
- package/src/commands/dev/bridge/index.js.map +1 -0
- package/src/commands/dev/bridge/log.d.ts +29 -0
- package/src/commands/dev/bridge/log.js +82 -0
- package/src/commands/dev/bridge/log.js.map +1 -0
- package/src/commands/dev/bridge/state-machine.d.ts +56 -0
- package/src/commands/dev/bridge/state-machine.js +245 -0
- package/src/commands/dev/bridge/state-machine.js.map +1 -0
- package/src/commands/dev/bridge/stdio-framer.d.ts +47 -0
- package/src/commands/dev/bridge/stdio-framer.js +128 -0
- package/src/commands/dev/bridge/stdio-framer.js.map +1 -0
- package/src/commands/dev/bridge/upstream-client.d.ts +49 -0
- package/src/commands/dev/bridge/upstream-client.js +159 -0
- package/src/commands/dev/bridge/upstream-client.js.map +1 -0
- package/src/commands/dev/bridge/watcher.d.ts +30 -0
- package/src/commands/dev/bridge/watcher.js +87 -0
- package/src/commands/dev/bridge/watcher.js.map +1 -0
- package/src/commands/dev/dev.d.ts +18 -1
- package/src/commands/dev/dev.js +134 -14
- package/src/commands/dev/dev.js.map +1 -1
- package/src/commands/dev/inspector.d.ts +13 -1
- package/src/commands/dev/inspector.js +77 -3
- package/src/commands/dev/inspector.js.map +1 -1
- package/src/commands/dev/port.d.ts +23 -0
- package/src/commands/dev/port.js +87 -0
- package/src/commands/dev/port.js.map +1 -0
- package/src/commands/dev/register.d.ts +1 -1
- package/src/commands/dev/register.js +28 -4
- package/src/commands/dev/register.js.map +1 -1
- package/src/commands/dev/test.d.ts +26 -1
- package/src/commands/dev/test.js +181 -64
- package/src/commands/dev/test.js.map +1 -1
- package/src/commands/eject/mcp-client.d.ts +25 -0
- package/src/commands/eject/mcp-client.js +74 -0
- package/src/commands/eject/mcp-client.js.map +1 -0
- package/src/commands/eject/register.d.ts +9 -0
- package/src/commands/eject/register.js +56 -0
- package/src/commands/eject/register.js.map +1 -0
- package/src/commands/install/install-claude-plugin.d.ts +13 -0
- package/src/commands/install/install-claude-plugin.js +327 -0
- package/src/commands/install/install-claude-plugin.js.map +1 -0
- package/src/commands/install/register.d.ts +16 -0
- package/src/commands/install/register.js +70 -0
- package/src/commands/install/register.js.map +1 -0
- package/src/commands/scaffold/create.js +44 -0
- package/src/commands/scaffold/create.js.map +1 -1
- package/src/commands/skills/from-entry.d.ts +31 -0
- package/src/commands/skills/from-entry.js +68 -0
- package/src/commands/skills/from-entry.js.map +1 -0
- package/src/commands/skills/install.d.ts +12 -0
- package/src/commands/skills/install.js +173 -8
- package/src/commands/skills/install.js.map +1 -1
- package/src/commands/skills/register.js +7 -3
- package/src/commands/skills/register.js.map +1 -1
- package/src/config/frontmcp-config.loader.d.ts +28 -0
- package/src/config/frontmcp-config.loader.js +146 -67
- package/src/config/frontmcp-config.loader.js.map +1 -1
- package/src/config/frontmcp-config.resolve.d.ts +67 -0
- package/src/config/frontmcp-config.resolve.js +118 -0
- package/src/config/frontmcp-config.resolve.js.map +1 -0
- package/src/config/frontmcp-config.schema.d.ts +207 -0
- package/src/config/frontmcp-config.schema.js +217 -1
- package/src/config/frontmcp-config.schema.js.map +1 -1
- package/src/config/frontmcp-config.types.d.ts +133 -0
- package/src/config/frontmcp-config.types.js.map +1 -1
- package/src/config/index.d.ts +2 -1
- package/src/config/index.js +3 -1
- package/src/config/index.js.map +1 -1
- package/src/core/args.d.ts +13 -0
- package/src/core/args.js.map +1 -1
- package/src/core/bridge.js +39 -0
- package/src/core/bridge.js.map +1 -1
- package/src/core/cli.d.ts +0 -6
- package/src/core/cli.js +23 -3
- package/src/core/cli.js.map +1 -1
- package/src/core/help.d.ts +1 -1
- package/src/core/help.js +27 -6
- package/src/core/help.js.map +1 -1
- package/src/core/program.d.ts +1 -1
- package/src/core/program.js +56 -12
- package/src/core/program.js.map +1 -1
- package/src/core/project-commands.d.ts +44 -0
- package/src/core/project-commands.js +216 -0
- package/src/core/project-commands.js.map +1 -0
package/src/commands/dev/dev.js
CHANGED
|
@@ -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
|
-
|
|
23
|
-
//
|
|
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
|
-
//
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
145
|
+
env: childEnv,
|
|
36
146
|
});
|
|
37
|
-
const checker = (0, child_process_1.spawn)(
|
|
147
|
+
const checker = (0, child_process_1.spawn)(npxCmd, ['-y', 'tsc', '--noEmit', '--pretty', '--watch'], {
|
|
38
148
|
stdio: 'inherit',
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":";;
|
|
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
|
-
.
|
|
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
|
-
|
|
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":";;
|
|
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
|
*
|