@mandujs/mcp 0.18.8 → 0.18.10
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/README.md +0 -1
- package/package.json +42 -42
- package/src/activity-monitor.ts +9 -8
- package/src/adapters/tool-adapter.ts +2 -0
- package/src/executor/error-handler.ts +268 -250
- package/src/index.ts +8 -0
- package/src/new-resources.ts +119 -0
- package/src/profiles.ts +34 -0
- package/src/prompts.ts +104 -0
- package/src/resources/handlers.ts +0 -23
- package/src/server.ts +78 -5
- package/src/tools/ate.ts +28 -0
- package/src/tools/brain.ts +56 -24
- package/src/tools/component.ts +194 -185
- package/src/tools/composite.ts +440 -0
- package/src/tools/contract.ts +58 -58
- package/src/tools/decisions.ts +270 -0
- package/src/tools/generate.ts +23 -21
- package/src/tools/guard.ts +32 -708
- package/src/tools/history.ts +24 -7
- package/src/tools/hydration.ts +40 -13
- package/src/tools/index.ts +28 -2
- package/src/tools/kitchen.ts +107 -0
- package/src/tools/negotiate.ts +263 -0
- package/src/tools/project.ts +464 -382
- package/src/tools/resource.ts +19 -2
- package/src/tools/runtime.ts +533 -508
- package/src/tools/seo.ts +446 -417
- package/src/tools/slot-validation.ts +200 -0
- package/src/tools/slot.ts +20 -21
- package/src/tools/spec.ts +45 -43
- package/src/tools/transaction.ts +55 -13
- package/src/tx-lock.ts +73 -0
- package/src/utils/project.ts +48 -9
- package/src/utils/runtime-control.ts +52 -0
- package/src/utils/withWarnings.ts +2 -1
package/src/tx-lock.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transaction Lock — prevents concurrent project mutations from multiple AI agents.
|
|
3
|
+
*
|
|
4
|
+
* In-process singleton. If no lock exists, destructive tools work as before (backward compatible).
|
|
5
|
+
* When a lock is held, only the holder (matching lockId) may execute destructive operations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface TxLock {
|
|
9
|
+
lockId: string;
|
|
10
|
+
sessionId: string;
|
|
11
|
+
acquiredAt: number;
|
|
12
|
+
timeoutMs: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
16
|
+
|
|
17
|
+
let activeLock: TxLock | null = null;
|
|
18
|
+
|
|
19
|
+
function isExpired(lock: TxLock): boolean {
|
|
20
|
+
return Date.now() - lock.acquiredAt > lock.timeoutMs;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function acquireLock(
|
|
24
|
+
sessionId: string,
|
|
25
|
+
timeoutMs: number = DEFAULT_TIMEOUT_MS,
|
|
26
|
+
): { success: boolean; lockId?: string; error?: string } {
|
|
27
|
+
if (activeLock) {
|
|
28
|
+
if (isExpired(activeLock)) {
|
|
29
|
+
activeLock = null; // auto-release stale lock
|
|
30
|
+
} else {
|
|
31
|
+
return {
|
|
32
|
+
success: false,
|
|
33
|
+
error: `Lock held by session "${activeLock.sessionId}" since ${new Date(activeLock.acquiredAt).toISOString()}`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const lockId = crypto.randomUUID();
|
|
38
|
+
activeLock = { lockId, sessionId, acquiredAt: Date.now(), timeoutMs };
|
|
39
|
+
return { success: true, lockId };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function releaseLock(lockId: string): boolean {
|
|
43
|
+
if (!activeLock || activeLock.lockId !== lockId) return false;
|
|
44
|
+
activeLock = null;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function checkLock(): {
|
|
49
|
+
locked: boolean;
|
|
50
|
+
lockId?: string;
|
|
51
|
+
sessionId?: string;
|
|
52
|
+
acquiredAt?: number;
|
|
53
|
+
} {
|
|
54
|
+
if (activeLock && isExpired(activeLock)) {
|
|
55
|
+
activeLock = null;
|
|
56
|
+
}
|
|
57
|
+
if (!activeLock) return { locked: false };
|
|
58
|
+
return {
|
|
59
|
+
locked: true,
|
|
60
|
+
lockId: activeLock.lockId,
|
|
61
|
+
sessionId: activeLock.sessionId,
|
|
62
|
+
acquiredAt: activeLock.acquiredAt,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function requireLock(lockId?: string): { allowed: boolean; error?: string } {
|
|
67
|
+
if (!activeLock || isExpired(activeLock)) return { allowed: true };
|
|
68
|
+
if (lockId === activeLock.lockId) return { allowed: true };
|
|
69
|
+
return {
|
|
70
|
+
allowed: false,
|
|
71
|
+
error: `Project is locked by session "${activeLock.sessionId}". Provide a matching lockId or wait for expiry.`,
|
|
72
|
+
};
|
|
73
|
+
}
|
package/src/utils/project.ts
CHANGED
|
@@ -3,19 +3,44 @@ import fs from "fs/promises";
|
|
|
3
3
|
import { pathToFileURL } from "url";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Find the Mandu project root by looking for app/ directory
|
|
6
|
+
* Find the Mandu project root by looking for mandu.config.* or app/ directory.
|
|
7
|
+
*
|
|
8
|
+
* Detection order (first match wins):
|
|
9
|
+
* 1. mandu.config.ts / .js / .json in the current directory
|
|
10
|
+
* 2. app/ directory in the current directory
|
|
11
|
+
* 3. Walk up to parent directories and repeat
|
|
12
|
+
*
|
|
13
|
+
* For monorepo sub-projects (e.g. demo/ai-chat inside a larger workspace),
|
|
14
|
+
* the config file takes priority so that the MCP server binds to the correct
|
|
15
|
+
* sub-project even when launched from the monorepo root.
|
|
16
|
+
*
|
|
17
|
+
* ## Monorepo Sub-Project Setup
|
|
18
|
+
*
|
|
19
|
+
* When using MCP in a monorepo sub-project, the recommended `.mcp.json` is:
|
|
20
|
+
*
|
|
21
|
+
* ```json
|
|
22
|
+
* {
|
|
23
|
+
* "mcpServers": {
|
|
24
|
+
* "mandu": {
|
|
25
|
+
* "command": "bun",
|
|
26
|
+
* "args": ["run", "node_modules/@mandujs/mcp/src/index.ts"],
|
|
27
|
+
* "cwd": "."
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* The sub-project must have `@mandujs/mcp` as a devDependency.
|
|
34
|
+
* Using a `cwd` that points to a parent monorepo root will NOT work because
|
|
35
|
+
* the MCP stdio transport resolves paths relative to the spawned process,
|
|
36
|
+
* and the parent's node_modules layout differs from the sub-project's.
|
|
7
37
|
*/
|
|
8
38
|
export async function findProjectRoot(startDir: string = process.cwd()): Promise<string | null> {
|
|
9
39
|
let currentDir = path.resolve(startDir);
|
|
10
40
|
|
|
11
41
|
while (currentDir !== path.dirname(currentDir)) {
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
const appStat = await fs.stat(path.join(currentDir, "app"));
|
|
15
|
-
if (appStat.isDirectory()) return currentDir;
|
|
16
|
-
} catch {}
|
|
17
|
-
|
|
18
|
-
// Check for mandu.config.* files
|
|
42
|
+
// Prioritize config files — they unambiguously mark a Mandu project root,
|
|
43
|
+
// which is critical for monorepo sub-projects that each have their own config.
|
|
19
44
|
for (const configFile of ["mandu.config.ts", "mandu.config.js", "mandu.config.json"]) {
|
|
20
45
|
try {
|
|
21
46
|
await fs.access(path.join(currentDir, configFile));
|
|
@@ -23,6 +48,21 @@ export async function findProjectRoot(startDir: string = process.cwd()): Promise
|
|
|
23
48
|
} catch {}
|
|
24
49
|
}
|
|
25
50
|
|
|
51
|
+
// Check for app/ directory (FS Routes source)
|
|
52
|
+
try {
|
|
53
|
+
const appStat = await fs.stat(path.join(currentDir, "app"));
|
|
54
|
+
if (appStat.isDirectory()) {
|
|
55
|
+
// Extra guard: only treat this as a Mandu project if it also has
|
|
56
|
+
// package.json (avoids false positives from unrelated app/ dirs)
|
|
57
|
+
try {
|
|
58
|
+
await fs.access(path.join(currentDir, "package.json"));
|
|
59
|
+
return currentDir;
|
|
60
|
+
} catch {
|
|
61
|
+
// No package.json — likely not a Mandu project, keep searching
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch {}
|
|
65
|
+
|
|
26
66
|
currentDir = path.dirname(currentDir);
|
|
27
67
|
}
|
|
28
68
|
|
|
@@ -38,7 +78,6 @@ export function getProjectPaths(rootDir: string) {
|
|
|
38
78
|
appDir: path.join(rootDir, "app"),
|
|
39
79
|
specDir: path.join(rootDir, "spec"),
|
|
40
80
|
manifestPath: path.join(rootDir, ".mandu", "routes.manifest.json"),
|
|
41
|
-
lockPath: path.join(rootDir, ".mandu", "spec.lock.json"),
|
|
42
81
|
slotsDir: path.join(rootDir, "spec", "slots"),
|
|
43
82
|
contractsDir: path.join(rootDir, "spec", "contracts"),
|
|
44
83
|
historyDir: path.join(rootDir, ".mandu", "history"),
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
|
|
3
|
+
export interface RuntimeControlRecord {
|
|
4
|
+
mode: "dev" | "start";
|
|
5
|
+
port: number;
|
|
6
|
+
token: string;
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
startedAt: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const RUNTIME_CONTROL_RELATIVE_PATH = path.join(".mandu", "runtime-control.json");
|
|
12
|
+
|
|
13
|
+
export async function readRuntimeControl(rootDir: string): Promise<RuntimeControlRecord | null> {
|
|
14
|
+
try {
|
|
15
|
+
const file = Bun.file(path.join(rootDir, RUNTIME_CONTROL_RELATIVE_PATH));
|
|
16
|
+
if (!(await file.exists())) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return await file.json() as RuntimeControlRecord;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function requestRuntimeCache(
|
|
26
|
+
rootDir: string,
|
|
27
|
+
action: "stats" | "clear",
|
|
28
|
+
payload: Record<string, unknown> = {}
|
|
29
|
+
): Promise<{ control: RuntimeControlRecord; response: Response; body: unknown } | null> {
|
|
30
|
+
const control = await readRuntimeControl(rootDir);
|
|
31
|
+
if (!control) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const response = await fetch(`${control.baseUrl}/_mandu/cache`, {
|
|
36
|
+
method: action === "stats" ? "GET" : "POST",
|
|
37
|
+
headers: {
|
|
38
|
+
"Content-Type": "application/json",
|
|
39
|
+
"x-mandu-control-token": control.token,
|
|
40
|
+
},
|
|
41
|
+
...(action === "clear" ? { body: JSON.stringify(payload) } : {}),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
let body: unknown = null;
|
|
45
|
+
try {
|
|
46
|
+
body = await response.json();
|
|
47
|
+
} catch {
|
|
48
|
+
// ignore invalid JSON
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { control, response, body };
|
|
52
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Mutation 도구(write_slot, add_route, generate 등) 실행 후
|
|
5
5
|
* watcher 경고를 자동으로 응답에 포함시킨다.
|
|
6
6
|
*
|
|
7
|
-
* MCP notification이
|
|
7
|
+
* MCP notification이 AI 에이전트에 전달되지 않는 문제를 해결.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { getWatcher } from "../../../core/src/index.js";
|
|
@@ -14,6 +14,7 @@ const MUTATION_TOOLS = new Set([
|
|
|
14
14
|
"mandu_add_route",
|
|
15
15
|
"mandu_update_route",
|
|
16
16
|
"mandu_delete_route",
|
|
17
|
+
"mandu.generate",
|
|
17
18
|
"mandu_generate",
|
|
18
19
|
"mandu_build",
|
|
19
20
|
"mandu_commit",
|