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 getAvailablePort(startPort: number): Promise<number> {
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
- bsPort = await getAvailablePort(5090);
138
- pythonPort = await getAvailablePort(bsPort + 10);
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 buildArgs(serverSpec: string): string[] {
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),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-caspian-app",
3
- "version": "0.2.0-beta.35",
3
+ "version": "0.2.0-beta.36",
4
4
  "description": "Scaffold a new Caspian project (FastAPI-powered reactive Python framework).",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",