@mandujs/mcp 0.29.0 → 0.31.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 +3 -3
- package/src/resources/skills/loader.ts +218 -218
- package/src/resources/skills/mandu-deployment/rules/db-provider-supabase.md +300 -0
- package/src/server.ts +2 -1
- package/src/tools/ai-brief.ts +443 -443
- package/src/tools/ate-run.ts +13 -2
- package/src/tools/ate.ts +52 -3
- package/src/tools/brain.ts +37 -1
- package/src/tools/decisions.ts +270 -270
- package/src/tools/docs.ts +349 -0
- package/src/tools/extract-contract.ts +406 -406
- package/src/tools/guard.ts +56 -3
- package/src/tools/index.ts +4 -0
- package/src/tools/migrate-route-conventions.ts +345 -345
- package/src/tools/project.ts +128 -35
- package/src/tools/resource.ts +2 -1
- package/src/tools/rewrite-generated-barrel.ts +403 -403
- package/src/resources/skills/mandu-deployment/rules/deploy-platform-supabase.md +0 -323
package/src/tools/project.ts
CHANGED
|
@@ -11,9 +11,93 @@ import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
11
11
|
import type { ActivityMonitor } from "../activity-monitor.js";
|
|
12
12
|
import { spawn, type Subprocess } from "bun";
|
|
13
13
|
import { execSync } from "child_process";
|
|
14
|
+
import { createConnection } from "node:net";
|
|
14
15
|
import path from "path";
|
|
15
16
|
import fs from "fs/promises";
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Issue #237 Concern 3 — read `server.port` from `mandu.config.*` so
|
|
20
|
+
* `mandu.dev.start` can poll a deterministic port instead of timing
|
|
21
|
+
* out while scraping stdout. Returns `null` if the config is absent
|
|
22
|
+
* or doesn't explicitly set `server.port` (callers fall back to 3333,
|
|
23
|
+
* Mandu's documented default). We use the un-schema'd raw loader
|
|
24
|
+
* (`loadManduConfig`) so the schema's fill-in default doesn't mask a
|
|
25
|
+
* missing value — a user who set no port should poll 3333, not the
|
|
26
|
+
* schema's internal default.
|
|
27
|
+
*
|
|
28
|
+
* We intentionally catch every error — a brittle config reader here
|
|
29
|
+
* must never block dev_start. The polling path still proves liveness.
|
|
30
|
+
*/
|
|
31
|
+
export async function readConfiguredServerPort(
|
|
32
|
+
cwd: string,
|
|
33
|
+
): Promise<number | null> {
|
|
34
|
+
try {
|
|
35
|
+
const core = await import("@mandujs/core");
|
|
36
|
+
const raw = await core.loadManduConfig(cwd);
|
|
37
|
+
const port = raw?.server?.port;
|
|
38
|
+
if (typeof port === "number" && Number.isFinite(port) && port > 0) {
|
|
39
|
+
return port;
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
/* ignore — fall back to default */
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Issue #237 Concern 3 — TCP connect probe. Resolves `true` on the
|
|
49
|
+
* first successful `connect`, `false` on any error or timeout.
|
|
50
|
+
* `node:net` is Node builtin and ships with Bun; no new dependency.
|
|
51
|
+
*/
|
|
52
|
+
export function probeTcpPort(
|
|
53
|
+
port: number,
|
|
54
|
+
hostname: string,
|
|
55
|
+
timeoutMs: number,
|
|
56
|
+
): Promise<boolean> {
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
const sock = createConnection({ host: hostname, port });
|
|
59
|
+
const done = (ok: boolean) => {
|
|
60
|
+
try {
|
|
61
|
+
sock.destroy();
|
|
62
|
+
} catch {
|
|
63
|
+
/* noop */
|
|
64
|
+
}
|
|
65
|
+
resolve(ok);
|
|
66
|
+
};
|
|
67
|
+
sock.setTimeout(timeoutMs);
|
|
68
|
+
sock.once("connect", () => done(true));
|
|
69
|
+
sock.once("timeout", () => done(false));
|
|
70
|
+
sock.once("error", () => done(false));
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Issue #237 Concern 3 — poll the configured port at a fixed interval
|
|
76
|
+
* until `waitMs` elapses. Returns the port on success, `null` on
|
|
77
|
+
* timeout. The caller chooses whether to fall back to the stdout
|
|
78
|
+
* scrape or report `port: <polled>` alongside the timeout message.
|
|
79
|
+
*/
|
|
80
|
+
export async function pollServerPort(
|
|
81
|
+
port: number,
|
|
82
|
+
hostname: string,
|
|
83
|
+
waitMs: number,
|
|
84
|
+
intervalMs = 200,
|
|
85
|
+
): Promise<number | null> {
|
|
86
|
+
const deadline = Date.now() + waitMs;
|
|
87
|
+
while (Date.now() < deadline) {
|
|
88
|
+
const remaining = deadline - Date.now();
|
|
89
|
+
const probeTimeout = Math.min(500, Math.max(50, remaining));
|
|
90
|
+
if (await probeTcpPort(port, hostname, probeTimeout)) {
|
|
91
|
+
return port;
|
|
92
|
+
}
|
|
93
|
+
const sleep = Math.min(intervalMs, Math.max(0, deadline - Date.now()));
|
|
94
|
+
if (sleep > 0) {
|
|
95
|
+
await new Promise((r) => setTimeout(r, sleep));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
17
101
|
type DevServerState = {
|
|
18
102
|
process: Subprocess;
|
|
19
103
|
cwd: string;
|
|
@@ -173,7 +257,11 @@ export const projectToolDefinitions: Tool[] = [
|
|
|
173
257
|
},
|
|
174
258
|
{
|
|
175
259
|
name: "mandu.dev.start",
|
|
176
|
-
description:
|
|
260
|
+
description:
|
|
261
|
+
"Start Mandu dev server (bun run dev). Issue #237 — polls server.port from " +
|
|
262
|
+
"mandu.config.ts (fallback 3333) via TCP connect for up to waitMs (default 15s) " +
|
|
263
|
+
"before declaring a port-detection timeout. On success: { port, url, message }. " +
|
|
264
|
+
"On timeout: still returns { port: <polled>, message } so callers can retry / probe.",
|
|
177
265
|
annotations: {
|
|
178
266
|
readOnlyHint: false,
|
|
179
267
|
},
|
|
@@ -184,6 +272,12 @@ export const projectToolDefinitions: Tool[] = [
|
|
|
184
272
|
type: "string",
|
|
185
273
|
description: "Project directory to run dev server in (default: current project)",
|
|
186
274
|
},
|
|
275
|
+
waitMs: {
|
|
276
|
+
type: "number",
|
|
277
|
+
description:
|
|
278
|
+
"How long (ms) to wait for the dev server to accept TCP connections on " +
|
|
279
|
+
"the configured port. Default 15000 (15s).",
|
|
280
|
+
},
|
|
187
281
|
},
|
|
188
282
|
required: [],
|
|
189
283
|
},
|
|
@@ -318,7 +412,7 @@ export function projectTools(projectRoot: string, server?: Server, monitor?: Act
|
|
|
318
412
|
},
|
|
319
413
|
|
|
320
414
|
"mandu.dev.start": async (args: Record<string, unknown>) => {
|
|
321
|
-
const { cwd } = args as { cwd?: string };
|
|
415
|
+
const { cwd, waitMs } = args as { cwd?: string; waitMs?: number };
|
|
322
416
|
if (devServerState || devServerStarting) {
|
|
323
417
|
return {
|
|
324
418
|
success: false,
|
|
@@ -334,6 +428,22 @@ export function projectTools(projectRoot: string, server?: Server, monitor?: Act
|
|
|
334
428
|
try {
|
|
335
429
|
const targetDir = cwd ? path.resolve(projectRoot, cwd) : projectRoot;
|
|
336
430
|
|
|
431
|
+
// Issue #237 Concern 3 — read `server.port` from mandu.config.*
|
|
432
|
+
// so we can poll a deterministic port instead of racing a
|
|
433
|
+
// regex against stdout. The env override takes precedence (it
|
|
434
|
+
// also takes precedence in the CLI — see cli/commands/dev.ts).
|
|
435
|
+
// Fall back to 3333 (Mandu's default) when neither is set.
|
|
436
|
+
const envPort = process.env.PORT ? Number(process.env.PORT) : null;
|
|
437
|
+
const configPort =
|
|
438
|
+
envPort && Number.isFinite(envPort)
|
|
439
|
+
? envPort
|
|
440
|
+
: await readConfiguredServerPort(targetDir);
|
|
441
|
+
const polledPort = configPort ?? 3333;
|
|
442
|
+
const pollWaitMs =
|
|
443
|
+
typeof waitMs === "number" && Number.isFinite(waitMs) && waitMs > 0
|
|
444
|
+
? waitMs
|
|
445
|
+
: 15_000;
|
|
446
|
+
|
|
337
447
|
const proc = spawn(["bun", "run", "dev"], {
|
|
338
448
|
cwd: targetDir,
|
|
339
449
|
stdout: "pipe",
|
|
@@ -350,32 +460,6 @@ export function projectTools(projectRoot: string, server?: Server, monitor?: Act
|
|
|
350
460
|
};
|
|
351
461
|
devServerState = state;
|
|
352
462
|
|
|
353
|
-
// Wait for the server to output its port before returning
|
|
354
|
-
const portPromise = new Promise<{ port: number; url: string } | null>((resolve) => {
|
|
355
|
-
const PORT_DETECT_TIMEOUT_MS = 15_000;
|
|
356
|
-
const timeout = setTimeout(() => resolve(null), PORT_DETECT_TIMEOUT_MS);
|
|
357
|
-
const portPattern = /https?:\/\/[^:\s]+:(\d+)/;
|
|
358
|
-
|
|
359
|
-
const originalPush = state.output.push.bind(state.output);
|
|
360
|
-
state.output.push = (...items: string[]) => {
|
|
361
|
-
const result = originalPush(...items);
|
|
362
|
-
for (const item of items) {
|
|
363
|
-
const match = item.match(portPattern);
|
|
364
|
-
if (match) {
|
|
365
|
-
const detectedPort = parseInt(match[1], 10);
|
|
366
|
-
clearTimeout(timeout);
|
|
367
|
-
state.output.push = originalPush;
|
|
368
|
-
resolve({
|
|
369
|
-
port: detectedPort,
|
|
370
|
-
url: match[0],
|
|
371
|
-
});
|
|
372
|
-
break;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
return result;
|
|
376
|
-
};
|
|
377
|
-
});
|
|
378
|
-
|
|
379
463
|
consumeStream(proc.stdout, state, "stdout", server).catch(() => {});
|
|
380
464
|
consumeStream(proc.stderr, state, "stderr", server).catch(() => {});
|
|
381
465
|
|
|
@@ -389,19 +473,28 @@ export function projectTools(projectRoot: string, server?: Server, monitor?: Act
|
|
|
389
473
|
monitor.logEvent("dev", `Dev server started (${targetDir})`);
|
|
390
474
|
}
|
|
391
475
|
|
|
392
|
-
//
|
|
393
|
-
|
|
476
|
+
// Issue #237 Concern 3 — TCP poll the expected port. 127.0.0.1
|
|
477
|
+
// matches what the CLI prints; dual-stack (`::`) binds accept
|
|
478
|
+
// loopback v4 connects. We use 127.0.0.1 because `localhost`
|
|
479
|
+
// resolution varies across Windows + macOS.
|
|
480
|
+
const detectedPort = await pollServerPort(
|
|
481
|
+
polledPort,
|
|
482
|
+
"127.0.0.1",
|
|
483
|
+
pollWaitMs,
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
const url = detectedPort ? `http://localhost:${detectedPort}` : null;
|
|
394
487
|
|
|
395
488
|
return {
|
|
396
489
|
success: true,
|
|
397
490
|
pid: proc.pid,
|
|
398
|
-
port:
|
|
399
|
-
url
|
|
491
|
+
port: detectedPort ?? polledPort,
|
|
492
|
+
url,
|
|
400
493
|
cwd: targetDir,
|
|
401
494
|
startedAt: state.startedAt.toISOString(),
|
|
402
|
-
message:
|
|
403
|
-
? `Dev server
|
|
404
|
-
:
|
|
495
|
+
message: detectedPort
|
|
496
|
+
? `Dev server ready at http://localhost:${detectedPort}`
|
|
497
|
+
: `Dev server started (port detection timed out after ${pollWaitMs}ms polling ${polledPort})`,
|
|
405
498
|
};
|
|
406
499
|
} finally {
|
|
407
500
|
devServerStarting = false;
|
package/src/tools/resource.ts
CHANGED
|
@@ -318,7 +318,8 @@ async function readResourceDefinition(
|
|
|
318
318
|
return parsed.definition;
|
|
319
319
|
} catch (error) {
|
|
320
320
|
throw new Error(
|
|
321
|
-
`Failed to parse resource schema: ${error instanceof Error ? error.message : String(error)}
|
|
321
|
+
`Failed to parse resource schema: ${error instanceof Error ? error.message : String(error)}`,
|
|
322
|
+
{ cause: error }
|
|
322
323
|
);
|
|
323
324
|
}
|
|
324
325
|
}
|