create-caspian-app 0.2.0-beta.35 → 0.2.0-beta.36
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.
|
@@ -13,6 +13,7 @@ import { componentMap } from "./component-map.js";
|
|
|
13
13
|
import { createServer } from "net";
|
|
14
14
|
import chalk from "chalk";
|
|
15
15
|
import { networkInterfaces } from "os";
|
|
16
|
+
import caspianConfig from "../caspian.config.json";
|
|
16
17
|
|
|
17
18
|
const { __dirname } = getFileMeta();
|
|
18
19
|
const bs: BrowserSyncInstance = browserSync.create();
|
|
@@ -24,14 +25,48 @@ let lastChangedFile: string | null = null;
|
|
|
24
25
|
let pythonPort = 0;
|
|
25
26
|
let bsPort = 0;
|
|
26
27
|
|
|
27
|
-
function
|
|
28
|
+
function getReservedPorts(): Set<number> {
|
|
29
|
+
const reservedPorts = new Set<number>();
|
|
30
|
+
|
|
31
|
+
if (!caspianConfig.mcp) {
|
|
32
|
+
return reservedPorts;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const mcpSpecPath = join(__dirname, "..", "src", "lib", "mcp", "fastmcp.json");
|
|
36
|
+
if (!existsSync(mcpSpecPath)) {
|
|
37
|
+
return reservedPorts;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const mcpSpec = JSON.parse(readFileSync(mcpSpecPath, "utf-8"));
|
|
42
|
+
const reservedPort = Number(mcpSpec?.deployment?.port);
|
|
43
|
+
|
|
44
|
+
if (Number.isInteger(reservedPort) && reservedPort > 0) {
|
|
45
|
+
reservedPorts.add(reservedPort);
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Ignore malformed local MCP config and fall back to dynamic allocation.
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return reservedPorts;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getAvailablePort(
|
|
55
|
+
startPort: number,
|
|
56
|
+
reservedPorts: Set<number> = new Set(),
|
|
57
|
+
): Promise<number> {
|
|
28
58
|
return new Promise((resolve) => {
|
|
59
|
+
if (reservedPorts.has(startPort)) {
|
|
60
|
+
resolve(getAvailablePort(startPort + 1, reservedPorts));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
29
64
|
const server = createServer();
|
|
30
65
|
server.listen(startPort, () => {
|
|
31
66
|
server.close(() => resolve(startPort));
|
|
32
67
|
});
|
|
33
68
|
server.on("error", () => {
|
|
34
|
-
resolve(getAvailablePort(startPort + 1));
|
|
69
|
+
resolve(getAvailablePort(startPort + 1, reservedPorts));
|
|
35
70
|
});
|
|
36
71
|
});
|
|
37
72
|
}
|
|
@@ -134,8 +169,10 @@ const publicPipeline = new DebouncedWorker(
|
|
|
134
169
|
);
|
|
135
170
|
|
|
136
171
|
(async () => {
|
|
137
|
-
|
|
138
|
-
|
|
172
|
+
const reservedPorts = getReservedPorts();
|
|
173
|
+
|
|
174
|
+
bsPort = await getAvailablePort(5090, reservedPorts);
|
|
175
|
+
pythonPort = await getAvailablePort(5200, reservedPorts);
|
|
139
176
|
|
|
140
177
|
updateRouteFilesCache();
|
|
141
178
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { existsSync } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { isAbsolute, join } from "path";
|
|
3
|
+
import { createServer } from "net";
|
|
3
4
|
import caspianConfig from "../caspian.config.json";
|
|
4
5
|
import { createRestartableProcess, onExit } from "./utils.js";
|
|
5
6
|
|
|
@@ -36,11 +37,79 @@ function getServerSpec(): string | null {
|
|
|
36
37
|
return null;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
function
|
|
40
|
+
function getServerSpecPath(serverSpec: string): string | null {
|
|
41
|
+
if (serverSpec.startsWith("http://") || serverSpec.startsWith("https://")) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return isAbsolute(serverSpec) ? serverSpec : join(projectRoot, serverSpec);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readDeploymentConfig(serverSpec: string): Record<string, unknown> {
|
|
49
|
+
const serverSpecPath = getServerSpecPath(serverSpec);
|
|
50
|
+
if (!serverSpecPath || !existsSync(serverSpecPath)) {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const rawConfig = JSON.parse(readFileSync(serverSpecPath, "utf-8"));
|
|
56
|
+
if (rawConfig && typeof rawConfig === "object") {
|
|
57
|
+
const deployment = rawConfig.deployment;
|
|
58
|
+
if (deployment && typeof deployment === "object") {
|
|
59
|
+
return deployment as Record<string, unknown>;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
// Ignore malformed specs and allow FastMCP to validate them.
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getPreferredHost(serverSpec: string): string {
|
|
70
|
+
return (
|
|
71
|
+
process.env.MCP_HOST?.trim() ||
|
|
72
|
+
String(readDeploymentConfig(serverSpec).host || "").trim() ||
|
|
73
|
+
"127.0.0.1"
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getPreferredPort(serverSpec: string): number | null {
|
|
78
|
+
const rawPort =
|
|
79
|
+
process.env.MCP_PORT?.trim() ||
|
|
80
|
+
String(readDeploymentConfig(serverSpec).port || "").trim();
|
|
81
|
+
|
|
82
|
+
if (!rawPort) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const port = Number(rawPort);
|
|
87
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return port;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function findAvailablePort(startPort: number, host: string): Promise<number> {
|
|
95
|
+
return new Promise((resolve) => {
|
|
96
|
+
const server = createServer();
|
|
97
|
+
|
|
98
|
+
server.listen(startPort, host, () => {
|
|
99
|
+
server.close(() => resolve(startPort));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
server.on("error", () => {
|
|
103
|
+
resolve(findAvailablePort(startPort + 1, host));
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function buildArgs(serverSpec: string, portOverride?: string): string[] {
|
|
40
109
|
const args = ["run", serverSpec, "--no-banner"];
|
|
41
110
|
const transport = process.env.MCP_TRANSPORT?.trim();
|
|
42
111
|
const host = process.env.MCP_HOST?.trim();
|
|
43
|
-
const port = process.env.MCP_PORT?.trim();
|
|
112
|
+
const port = portOverride || process.env.MCP_PORT?.trim();
|
|
44
113
|
const path = process.env.MCP_PATH?.trim();
|
|
45
114
|
const logLevel = process.env.MCP_LOG_LEVEL?.trim();
|
|
46
115
|
|
|
@@ -133,12 +202,24 @@ if (!serverSpec) {
|
|
|
133
202
|
process.exit(0);
|
|
134
203
|
}
|
|
135
204
|
|
|
205
|
+
const preferredHost = getPreferredHost(serverSpec);
|
|
206
|
+
const preferredPort = getPreferredPort(serverSpec);
|
|
207
|
+
const resolvedPort = preferredPort
|
|
208
|
+
? await findAvailablePort(preferredPort, preferredHost)
|
|
209
|
+
: null;
|
|
210
|
+
|
|
211
|
+
if (preferredPort && resolvedPort && preferredPort !== resolvedPort) {
|
|
212
|
+
console.log(
|
|
213
|
+
`[mcp] Port ${preferredPort} is unavailable on ${preferredHost}, using ${resolvedPort} instead.`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
136
217
|
const handleMcpOutput = createMcpOutputHandler();
|
|
137
218
|
|
|
138
219
|
const runner = createRestartableProcess({
|
|
139
220
|
name: "mcp",
|
|
140
221
|
cmd: fastMcpCommand,
|
|
141
|
-
args: buildArgs(serverSpec),
|
|
222
|
+
args: buildArgs(serverSpec, resolvedPort ? String(resolvedPort) : undefined),
|
|
142
223
|
startMessage: "[mcp] Starting MCP server...",
|
|
143
224
|
onStdout: createLineHandler(handleMcpOutput),
|
|
144
225
|
onStderr: createLineHandler(handleMcpOutput),
|