@mandujs/mcp 0.9.46 → 0.10.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/README.md +23 -1
- package/package.json +1 -1
- package/src/index.ts +18 -7
- package/src/server.ts +27 -24
- package/src/tools/guard.ts +896 -1
- package/src/tools/project.ts +334 -0
- package/src/utils/project.ts +32 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mandu MCP - Project Tools
|
|
3
|
+
*
|
|
4
|
+
* - mandu_init: Create a new Mandu project (init + optional install)
|
|
5
|
+
* - mandu_dev_start: Start dev server
|
|
6
|
+
* - mandu_dev_stop: Stop dev server
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
+
import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
11
|
+
import type { ActivityMonitor } from "../activity-monitor.js";
|
|
12
|
+
import { spawn, type Subprocess } from "bun";
|
|
13
|
+
import path from "path";
|
|
14
|
+
import fs from "fs/promises";
|
|
15
|
+
|
|
16
|
+
type DevServerState = {
|
|
17
|
+
process: Subprocess;
|
|
18
|
+
cwd: string;
|
|
19
|
+
startedAt: Date;
|
|
20
|
+
output: string[];
|
|
21
|
+
maxLines: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
let devServerState: DevServerState | null = null;
|
|
25
|
+
|
|
26
|
+
function trimOutput(text: string, maxChars: number = 4000): string {
|
|
27
|
+
if (text.length <= maxChars) return text;
|
|
28
|
+
return text.slice(-maxChars);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function runCommand(cmd: string[], cwd: string) {
|
|
32
|
+
const proc = spawn(cmd, {
|
|
33
|
+
cwd,
|
|
34
|
+
stdout: "pipe",
|
|
35
|
+
stderr: "pipe",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
39
|
+
new Response(proc.stdout).text(),
|
|
40
|
+
new Response(proc.stderr).text(),
|
|
41
|
+
proc.exited,
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
exitCode,
|
|
46
|
+
stdout,
|
|
47
|
+
stderr,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function consumeStream(
|
|
52
|
+
stream: ReadableStream<Uint8Array> | null,
|
|
53
|
+
state: DevServerState,
|
|
54
|
+
label: "stdout" | "stderr",
|
|
55
|
+
server?: Server
|
|
56
|
+
) {
|
|
57
|
+
if (!stream) return;
|
|
58
|
+
const reader = stream.getReader();
|
|
59
|
+
const decoder = new TextDecoder();
|
|
60
|
+
let buffer = "";
|
|
61
|
+
|
|
62
|
+
while (true) {
|
|
63
|
+
const { done, value } = await reader.read();
|
|
64
|
+
if (done) break;
|
|
65
|
+
buffer += decoder.decode(value, { stream: true });
|
|
66
|
+
const lines = buffer.split(/\r?\n/);
|
|
67
|
+
buffer = lines.pop() ?? "";
|
|
68
|
+
|
|
69
|
+
for (const line of lines) {
|
|
70
|
+
const text = line.trim();
|
|
71
|
+
if (!text) continue;
|
|
72
|
+
const entry = `[${label}] ${text}`;
|
|
73
|
+
state.output.push(entry);
|
|
74
|
+
if (state.output.length > state.maxLines) {
|
|
75
|
+
state.output.shift();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (server) {
|
|
79
|
+
server.sendLoggingMessage({
|
|
80
|
+
level: label === "stderr" ? "warning" : "info",
|
|
81
|
+
logger: "mandu-dev",
|
|
82
|
+
data: {
|
|
83
|
+
type: "dev_log",
|
|
84
|
+
stream: label,
|
|
85
|
+
message: text,
|
|
86
|
+
},
|
|
87
|
+
}).catch(() => {});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (buffer.trim()) {
|
|
93
|
+
const entry = `[${label}] ${buffer.trim()}`;
|
|
94
|
+
state.output.push(entry);
|
|
95
|
+
if (state.output.length > state.maxLines) {
|
|
96
|
+
state.output.shift();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const projectToolDefinitions: Tool[] = [
|
|
102
|
+
{
|
|
103
|
+
name: "mandu_init",
|
|
104
|
+
description:
|
|
105
|
+
"Initialize a new Mandu project (runs `mandu init` and optionally `bun install`).",
|
|
106
|
+
inputSchema: {
|
|
107
|
+
type: "object",
|
|
108
|
+
properties: {
|
|
109
|
+
name: {
|
|
110
|
+
type: "string",
|
|
111
|
+
description: "Project name (directory name)",
|
|
112
|
+
},
|
|
113
|
+
parentDir: {
|
|
114
|
+
type: "string",
|
|
115
|
+
description: "Parent directory to create the project in (default: cwd)",
|
|
116
|
+
},
|
|
117
|
+
css: {
|
|
118
|
+
type: "string",
|
|
119
|
+
enum: ["tailwind", "panda", "none"],
|
|
120
|
+
description: "CSS framework (default: tailwind)",
|
|
121
|
+
},
|
|
122
|
+
ui: {
|
|
123
|
+
type: "string",
|
|
124
|
+
enum: ["shadcn", "ark", "none"],
|
|
125
|
+
description: "UI library (default: shadcn)",
|
|
126
|
+
},
|
|
127
|
+
theme: {
|
|
128
|
+
type: "boolean",
|
|
129
|
+
description: "Enable dark mode theme system",
|
|
130
|
+
},
|
|
131
|
+
minimal: {
|
|
132
|
+
type: "boolean",
|
|
133
|
+
description: "Create minimal template (no CSS/UI)",
|
|
134
|
+
},
|
|
135
|
+
install: {
|
|
136
|
+
type: "boolean",
|
|
137
|
+
description: "Run bun install after init (default: true)",
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
required: ["name"],
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "mandu_dev_start",
|
|
145
|
+
description: "Start Mandu dev server (bun run dev).",
|
|
146
|
+
inputSchema: {
|
|
147
|
+
type: "object",
|
|
148
|
+
properties: {
|
|
149
|
+
cwd: {
|
|
150
|
+
type: "string",
|
|
151
|
+
description: "Project directory to run dev server in (default: current project)",
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
required: [],
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: "mandu_dev_stop",
|
|
159
|
+
description: "Stop Mandu dev server if running.",
|
|
160
|
+
inputSchema: {
|
|
161
|
+
type: "object",
|
|
162
|
+
properties: {},
|
|
163
|
+
required: [],
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
export function projectTools(projectRoot: string, server?: Server, monitor?: ActivityMonitor) {
|
|
169
|
+
return {
|
|
170
|
+
mandu_init: async (args: Record<string, unknown>) => {
|
|
171
|
+
const {
|
|
172
|
+
name,
|
|
173
|
+
parentDir,
|
|
174
|
+
css,
|
|
175
|
+
ui,
|
|
176
|
+
theme,
|
|
177
|
+
minimal,
|
|
178
|
+
install = true,
|
|
179
|
+
} = args as {
|
|
180
|
+
name: string;
|
|
181
|
+
parentDir?: string;
|
|
182
|
+
css?: "tailwind" | "panda" | "none";
|
|
183
|
+
ui?: "shadcn" | "ark" | "none";
|
|
184
|
+
theme?: boolean;
|
|
185
|
+
minimal?: boolean;
|
|
186
|
+
install?: boolean;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const baseDir = parentDir
|
|
190
|
+
? path.resolve(projectRoot, parentDir)
|
|
191
|
+
: projectRoot;
|
|
192
|
+
|
|
193
|
+
await fs.mkdir(baseDir, { recursive: true });
|
|
194
|
+
|
|
195
|
+
const initArgs = ["@mandujs/cli", "init", name];
|
|
196
|
+
if (minimal) {
|
|
197
|
+
initArgs.push("--minimal");
|
|
198
|
+
} else {
|
|
199
|
+
if (css) initArgs.push("--css", css);
|
|
200
|
+
if (ui) initArgs.push("--ui", ui);
|
|
201
|
+
if (theme) initArgs.push("--theme");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const initResult = await runCommand(["bunx", ...initArgs], baseDir);
|
|
205
|
+
if (initResult.exitCode !== 0) {
|
|
206
|
+
return {
|
|
207
|
+
success: false,
|
|
208
|
+
step: "init",
|
|
209
|
+
exitCode: initResult.exitCode,
|
|
210
|
+
stdout: trimOutput(initResult.stdout),
|
|
211
|
+
stderr: trimOutput(initResult.stderr),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const projectDir = path.join(baseDir, name);
|
|
216
|
+
|
|
217
|
+
let installResult: { exitCode: number | null; stdout: string; stderr: string } | null = null;
|
|
218
|
+
if (install !== false) {
|
|
219
|
+
installResult = await runCommand(["bun", "install"], projectDir);
|
|
220
|
+
if (installResult.exitCode !== 0) {
|
|
221
|
+
return {
|
|
222
|
+
success: false,
|
|
223
|
+
step: "install",
|
|
224
|
+
projectDir,
|
|
225
|
+
exitCode: installResult.exitCode,
|
|
226
|
+
stdout: trimOutput(installResult.stdout),
|
|
227
|
+
stderr: trimOutput(installResult.stderr),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
success: true,
|
|
234
|
+
projectDir,
|
|
235
|
+
installed: install !== false,
|
|
236
|
+
init: {
|
|
237
|
+
exitCode: initResult.exitCode,
|
|
238
|
+
stdout: trimOutput(initResult.stdout),
|
|
239
|
+
stderr: trimOutput(initResult.stderr),
|
|
240
|
+
},
|
|
241
|
+
install: installResult
|
|
242
|
+
? {
|
|
243
|
+
exitCode: installResult.exitCode,
|
|
244
|
+
stdout: trimOutput(installResult.stdout),
|
|
245
|
+
stderr: trimOutput(installResult.stderr),
|
|
246
|
+
}
|
|
247
|
+
: null,
|
|
248
|
+
next: install !== false
|
|
249
|
+
? ["cd " + name, "bun run dev"]
|
|
250
|
+
: ["cd " + name, "bun install", "bun run dev"],
|
|
251
|
+
};
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
mandu_dev_start: async (args: Record<string, unknown>) => {
|
|
255
|
+
const { cwd } = args as { cwd?: string };
|
|
256
|
+
if (devServerState) {
|
|
257
|
+
return {
|
|
258
|
+
success: false,
|
|
259
|
+
message: "Dev server is already running",
|
|
260
|
+
pid: devServerState.process.pid,
|
|
261
|
+
cwd: devServerState.cwd,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const targetDir = cwd ? path.resolve(projectRoot, cwd) : projectRoot;
|
|
266
|
+
|
|
267
|
+
const proc = spawn(["bun", "run", "dev"], {
|
|
268
|
+
cwd: targetDir,
|
|
269
|
+
stdout: "pipe",
|
|
270
|
+
stderr: "pipe",
|
|
271
|
+
stdin: "ignore",
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const state: DevServerState = {
|
|
275
|
+
process: proc,
|
|
276
|
+
cwd: targetDir,
|
|
277
|
+
startedAt: new Date(),
|
|
278
|
+
output: [],
|
|
279
|
+
maxLines: 50,
|
|
280
|
+
};
|
|
281
|
+
devServerState = state;
|
|
282
|
+
|
|
283
|
+
consumeStream(proc.stdout, state, "stdout", server).catch(() => {});
|
|
284
|
+
consumeStream(proc.stderr, state, "stderr", server).catch(() => {});
|
|
285
|
+
|
|
286
|
+
proc.exited.then(() => {
|
|
287
|
+
if (devServerState?.process === proc) {
|
|
288
|
+
devServerState = null;
|
|
289
|
+
}
|
|
290
|
+
}).catch(() => {});
|
|
291
|
+
|
|
292
|
+
if (monitor) {
|
|
293
|
+
monitor.logEvent("dev", `Dev server started (${targetDir})`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
success: true,
|
|
298
|
+
pid: proc.pid,
|
|
299
|
+
cwd: targetDir,
|
|
300
|
+
startedAt: state.startedAt.toISOString(),
|
|
301
|
+
message: "Dev server started",
|
|
302
|
+
};
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
mandu_dev_stop: async () => {
|
|
306
|
+
if (!devServerState) {
|
|
307
|
+
return {
|
|
308
|
+
success: false,
|
|
309
|
+
message: "Dev server is not running",
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const { process: proc, cwd, output } = devServerState;
|
|
314
|
+
devServerState = null;
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
proc.kill();
|
|
318
|
+
} catch {
|
|
319
|
+
// ignore
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (monitor) {
|
|
323
|
+
monitor.logEvent("dev", `Dev server stopped (${cwd})`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
success: true,
|
|
328
|
+
message: "Dev server stopped",
|
|
329
|
+
cwd,
|
|
330
|
+
tail: output.slice(-10),
|
|
331
|
+
};
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
}
|
package/src/utils/project.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs/promises";
|
|
3
|
+
import { pathToFileURL } from "url";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Find the Mandu project root by looking for routes.manifest.json
|
|
@@ -69,3 +70,34 @@ export async function writeJsonFile(filePath: string, data: unknown): Promise<vo
|
|
|
69
70
|
await fs.mkdir(dir, { recursive: true });
|
|
70
71
|
await Bun.write(filePath, JSON.stringify(data, null, 2));
|
|
71
72
|
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Read Mandu config from mandu.config.ts/js/json
|
|
76
|
+
*/
|
|
77
|
+
export async function readConfig(rootDir: string): Promise<Record<string, unknown> | null> {
|
|
78
|
+
const configFiles = [
|
|
79
|
+
"mandu.config.ts",
|
|
80
|
+
"mandu.config.js",
|
|
81
|
+
"mandu.config.json",
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
for (const configFile of configFiles) {
|
|
85
|
+
const configPath = path.join(rootDir, configFile);
|
|
86
|
+
try {
|
|
87
|
+
const file = Bun.file(configPath);
|
|
88
|
+
if (await file.exists()) {
|
|
89
|
+
if (configFile.endsWith(".json")) {
|
|
90
|
+
return await file.json();
|
|
91
|
+
} else {
|
|
92
|
+
// For TS/JS files, use pathToFileURL for cross-platform compatibility
|
|
93
|
+
const module = await import(pathToFileURL(configPath).href);
|
|
94
|
+
return module.default ?? module;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return null;
|
|
103
|
+
}
|